Tracking Usage Events
Every time your AI agent does work for a customer — answers a question, sends an SMS, generates a report — you tell MarginFront about it by sending a usage event. MarginFront figures out the cost, rolls everything up at the end of the billing period, and generates an invoice. Think of it like a utility meter. Each event is a meter reading. MarginFront is the utility company that turns those readings into a bill. This page covers:- How to install and set up the SDK
- What fields to send with every event
- Four real-world examples (copy-paste ready)
- Batch events, error handling, and retry behavior
Install the SDK
Initialize the client
.connect() call required for usage tracking.
What fields do I send?
Required for EVERY event
| Field | Type | What it is |
|---|---|---|
customerExternalId | string | Your customer’s ID in your system. Whatever you use to identify them — "acme-001", "cust_abc123", etc. |
agentCode | string | The code for the agent/product that did the work. You set this in the MarginFront dashboard when you create an agent. Example: "cs-bot-v2". |
signalName | string | The metric being tracked. Matches the signal you set up in the dashboard. Examples: "messages", "sms-sent", "report-pages". |
model | string | The model or service that did the work. Pass whatever your provider returns. Examples: "gpt-4o", "claude-sonnet-4", "twilio-sms". |
modelProvider | string | The provider name, always lowercase. This tells MarginFront which pricing table to look in. Examples: "openai", "anthropic", "twilio", "google", "aws". |
Recommended for LLM events
| Field | Type | What it is |
|---|---|---|
inputTokens | number | The number of prompt tokens (what you sent to the model). Must be 0 or greater. |
outputTokens | number | The number of completion tokens (what the model sent back). Must be 0 or greater. |
For variable-quantity billing
| Field | Type | Default | What it is |
|---|---|---|---|
quantity | number | 1 | How many units of work happened. Use this for per-page, per-minute, per-SMS billing. If you bill the same amount regardless of volume, you can omit this (it defaults to 1). |
Optional
| Field | Type | Default | What it is |
|---|---|---|---|
usageDate | string (ISO 8601) or Date | now | When the work actually happened. Only needed if you’re back-filling historical events. Example: "2026-04-10T14:30:00Z". |
metadata | object | {} | Free-form key-value pairs. MarginFront stores them but does NOT use them for billing or cost calculation. Use metadata for your own debugging, analytics, or audit trail. |
Example 1: LLM Event (the 90% case)
Use case: Your AI customer support bot answers a question using GPT-4o via the OpenAI SDK. You need to track the token usage so MarginFront can calculate cost and bill your customer. Where does the MarginFront call go? After the OpenAI response comes back, in the response handler. You need the token counts from the response, so you can’t send the event before the LLM responds.| OpenAI response field | MarginFront field |
|---|---|
response.model | model |
response.usage.prompt_tokens | inputTokens |
response.usage.completion_tokens | outputTokens |
Example 2: Non-LLM Discrete Event (quantity = 1)
Use case: Your agent sends a Twilio SMS on behalf of a customer. There are no tokens involved — it’s a simple “one SMS was sent” event. Where does the MarginFront call go? After Twilio confirms the SMS was sent. You only want to bill for messages that actually went out.inputTokens or outputTokens here. Those fields are only for LLM calls. For non-LLM services, cost is based on quantity and the pricing you set up in the dashboard.
Example 3: Variable-Quantity Event (quantity = N)
Use case: Your agent generates a market research report for a customer. Reports vary in size — a 3-page report costs less than a 15-page report. You bill per page. This event has both token counts (because the report was generated by an LLM) and a quantity (because billing is based on pages, not tokens). The tokens track your cost from the LLM provider. The quantity tracks the output size for billing your customer.quantity: Any time the amount of work varies and you want billing to reflect that. Pages generated, minutes of audio transcribed, images produced, API calls batched — if the number changes per event, use quantity.
Example 4: Metadata
Use case: You want to attach debugging information to an event — which prompt template was used, which A/B test variant the customer saw, the conversation thread ID. This helps you analyze cost and performance later without affecting billing.- Strings, numbers, booleans, nested objects — any valid JSON.
- There is no schema. MarginFront stores whatever you send.
- Use it for audit trails, debugging, analytics, or linking events back to your own systems.
- It does not affect billing. A
customerTier: "enterprise"in metadata does not change the price. - It does not affect cost calculation. MarginFront ignores it completely for pricing.
- It does not appear on invoices.
Batch Events
When your agent does several things in quick succession (or you’re processing a queue), send them all in one request instead of one at a time. You can send 1 to 100 records per batch.Checking for partial failures
The API returns200 OK even when some records in the batch fail. Always check the response:
Fire-and-Forget Mode
By default,usage.record() and usage.recordBatch() run in fire-and-forget mode. This means:
- Your agent never blocks waiting for MarginFront. The call returns immediately.
- Network errors don’t crash your agent. If MarginFront is unreachable, the SDK puts the event into a retry buffer and tries again later.
- Validation errors log a warning and drop (they can’t be fixed by retrying).
How the retry buffer works
When a network error happens:- The failed event goes into an in-memory buffer (holds up to 1,000 events).
- The SDK retries the buffer on a backoff schedule: 10s, 20s, 40s, 60s (caps at 60s).
- Each event gets 5 retry attempts. After 5 failures, it’s dropped with a warning.
- When a retry succeeds, the backoff resets to 10s.
- If the buffer is full (1,000 events), the oldest event is dropped to make room.
Turning off fire-and-forget
If you want to handle errors yourself (for example, to log them to your own monitoring system), turn off fire-and-forget when you create the client:Field Reference
Required for every event
| Field | Type | Description |
|---|---|---|
customerExternalId | string | Your customer’s ID in your system. Must match the externalId you set when creating the customer in MarginFront (or via auto-provisioning). |
agentCode | string | The agent code from the MarginFront dashboard. Identifies which product/agent did the work. |
signalName | string | The name of the metric being tracked. Matches the signal you configured in the dashboard. |
model | string | The model or service identifier. Pass whatever your provider returns (e.g., response.model from OpenAI). Case-insensitive. |
modelProvider | string | The provider name, always lowercase. Tells MarginFront which pricing table to look up. |
Recommended for LLM events
| Field | Type | Description |
|---|---|---|
inputTokens | number | Number of prompt/input tokens. Must be >= 0. Required for MarginFront to calculate LLM cost. |
outputTokens | number | Number of completion/output tokens. Must be >= 0. Required for MarginFront to calculate LLM cost. |
For variable-quantity billing
| Field | Type | Default | Description |
|---|---|---|---|
quantity | number | 1 | Number of billing units. Use for per-page, per-minute, per-image, per-SMS billing. Must be >= 0. |
Optional
| Field | Type | Default | Description |
|---|---|---|---|
usageDate | string (ISO 8601) or Date | now | When the work happened. Use for back-filling historical events. |
metadata | object | {} | Free-form key-value pairs. Stored but NOT used for billing or cost calculation. |
Error Handling
The API returns 200 even when events fail
This is intentional. A batch of 10 events might have 9 successes and 1 failure. A200 tells you the request was received. The response body tells you what actually happened.
Always check failed > 0 in the response:
NEEDS_COST_BACKFILL — what it means
If you send amodel + modelProvider combination that MarginFront doesn’t recognize (not in the pricing table), this happens:
- The event is saved with
cost = null. It is NOT lost. - The response includes the event in
results.failedwith the codeNEEDS_COST_BACKFILLandstored: true. - In the MarginFront dashboard, go to the “Needs attention” section under Usage Events.
- Map the unrecognized model to a known one. MarginFront creates a permanent mapping and retroactively calculates cost for every event that used that model.
- Future events with that model auto-resolve. No manual step needed again.
stored: true. They are already saved. Retrying would create duplicates.
The “never break the core product” pattern
Your agent exists to serve your customers. MarginFront exists to bill for that work. If billing fails, the customer should still get served. Always wrap usage tracking in a try/catch:fireAndForget: true (the default) and the SDK handles this pattern for you automatically. The call never throws, never blocks, and retries in the background.
Common error codes
| Code | Event saved? | What happened | What to do |
|---|---|---|---|
NEEDS_COST_BACKFILL | Yes | The model+provider isn’t in the pricing table. Event saved with cost = null. | Do NOT retry. Map the model in the dashboard. Cost backfills automatically. |
INTERNAL_ERROR | No | Something broke on MarginFront’s side. | Safe to retry. |
VALIDATION_ERROR | No | A required field is missing or has the wrong type. | Fix the data and resend. Check the error message for which field. |
Quick checklist
Before you ship, make sure:- The SDK is initialized with your secret key (
mf_sk_*), not the publishable key -
customerExternalIdmatches the external ID you set up for each customer -
agentCodematches the agent code shown in the dashboard -
signalNamematches the signal name shown in the dashboard - For LLM events, you’re sending
inputTokensandoutputTokensfrom the provider response - The
mf.usage.record()call is after the work is done (after the LLM responds, after the SMS sends) - The call is wrapped in try/catch (or
fireAndForgetis ON, which is the default) - You’re not retrying events that came back with
stored: true

