Skip to main content

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.
You don’t need to set anything up first. When you fire an event with a new customerExternalId, agentCode, or signalName, MarginFront creates the customer, agent, or signal automatically. Your existing user IDs from your own database flow straight through. The dashboard updates the moment the event lands.
This page covers:
  1. How to install and set up the SDK
  2. What fields to send with every event
  3. Four real-world examples (copy-paste ready)
  4. Batch events, error handling, and retry behavior

Install the SDK

Initialize the client

That’s it. The client is ready to send events. No .connect() call required for usage tracking.

What fields do I send?

Required for EVERY event

MarginFront uses these to calculate cost. If you’re tracking an LLM call and you don’t send token counts, MarginFront can’t calculate the cost for that event.

Prompt-cache tokens (optional, LLM only)

If you use prompt caching, send the cache token counts so cache-heavy traffic is priced at the cheaper cache rate instead of the full input rate. Both fields are optional and additive — leave them off and cache contributes nothing, exactly as before.

For variable-quantity billing

Optional


Tagging events for cost classification

If your finance team wants to split your AI spend into Cost of Goods Sold (what it costs to serve paying customers) versus Research & Development (internal experiments and test runs), add an environment to each event:
The first time a signal sees an event that carries an environment, MarginFront sorts that signal into a cost category for you: This automatic sorting only happens while the signal is still unclassified. Once you set a category by hand in the dashboard, your choice sticks — later events still record their environment, but they no longer change the category. environment is optional. Leave it off and your events work exactly as before. You can see the breakdown on your dashboard’s Cost Management page.

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.
Key mapping from OpenAI’s response to MarginFront fields:

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.
No 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)

Before you read this example, the one rule that keeps your bill accurate: fire ONE event per business outcome, not one per page, minute, or token. A 50-page report is one event with quantity: 50. A 3-minute call is one event with quantity: 3. The quantity field exists so you don’t have to loop. Looping would multiply your invoice and flood your analytics. See Choosing your signal name and quantity for the full mental model and a three-way comparison.
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.
When to use 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.
What you can put in metadata:
  • 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.
What metadata does NOT do:
  • 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.

Example 5: Multi-Service Event (one outcome, multiple services)

Use case: Your cold-outreach agent finds a prospect via Exa, enriches them via Hunter.io, writes a personalized message via Claude Opus, and sends it via Pipedream. From the customer’s perspective that’s ONE outreach to ONE prospect. From your cost perspective four underlying services contributed. Why one event: Firing four separate mf.usage.record calls (one per service) used to be the only option, but it quadrupled the prospect on your dashboard, made margin math harder, and could have multiplied the customer’s invoice if all four events shared a signal. With the services[] shape, you fire ONE event per business outcome regardless of how many services contributed.
What lands in MarginFront:
  • ONE event in the live event feed.
  • The Cost tab’s “Cost by service” chart shows the rolled-up total split across all four services (Exa, Hunter, Claude Opus, Pipedream).
  • Customer’s invoice bills per signal (one outreach = one charge at your pricing plan rate), regardless of how many services contributed.

LLM services: tokens AND quantity together

LLM services[] entries can carry both inputTokens/outputTokens AND quantity at the same time. The cold-outreach example above does exactly this on the Claude entry. Three reasons you’d want both:
  1. Track the call count alongside tokens. If the same LLM service was invoked N times within one outcome (e.g., one outreach that triggered 3 Claude calls because of retries or chain-of-thought intermediate prompts), quantity: 3 records the call count for analytics; inputTokens/outputTokens carry the aggregated totals.
  2. Enable per-call pricing later. MarginFront’s cost calculation today is tokens-only for LLM models. If your provider charges per call AND per token (or you want to layer your own per-call markup), tracking call count is what enables that. (The service_pricing catalog’s per-call cost coverage for LLM providers is being audited as a follow-up.)
  3. Reporting sanity-check. Lets you compare “calls per outreach” to “tokens per outreach” in your own analytics or in the Cost tab’s byModel breakdown.
If you don’t care about tracking the call count, omit quantity on LLM entries (or leave it at the default 1). Cost calculation is unaffected today.

Two shapes, mutually exclusive

Send model + modelProvider OR send services[], never both, never neither. If you mix shapes, the SDK rejects the request before it leaves your code with a clear English error pointing at the fix.

Multiple calls of the same model in one event

If your agent makes two Claude calls for one outreach (e.g., a draft pass + a refinement pass that both contribute to the same message), you have two equally valid ways to record it: Option A: list each call as its own services[] entry. Each becomes a distinct cost line. Use this when you want per-call resolution (e.g., to compare draft cost vs refinement cost).
Option B: aggregate into one services[] entry with summed tokens and quantity = call count. Cleaner if you don’t need per-call resolution.
Both produce the same rolled-up parent cost. The first is more granular for debugging; the second is more compact. See Core Concepts: One event, multiple services for the conceptual mental model and Usage Events API reference for the full request/response schema.

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.
You can mix LLM events and non-LLM events in the same batch. Different customers, different agents — all fine.

Checking for partial failures

The API returns 200 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:
  1. The failed event goes into an in-memory buffer (holds up to 1,000 events).
  2. The SDK retries the buffer on a backoff schedule: 10s, 20s, 40s, 60s (caps at 60s).
  3. Each event gets 5 retry attempts. After 5 failures, it’s dropped with a warning.
  4. When a retry succeeds, the backoff resets to 10s.
  5. 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:
Recommendation: Leave fire-and-forget ON (the default). Your agent should never break because the billing API is down. The retry buffer handles transient issues, and permanent failures (like a bad API key) will show up in your server logs as warnings.

Field Reference

Required for every event

For variable-quantity billing

Optional


What happens after you send an event

Your agents can always fire events. MarginFront stores every event it receives, even when it can’t figure out the cost yet. The response tells you which of three things happened so you know whether any follow-up action is needed. Think of it like dropping off a package at the post office. The package is always accepted. Sometimes the label is complete and it ships right away. Sometimes a piece of info is missing and it waits in a holding bin until you fill in the blank. Nothing gets thrown out.

The three states

Why this exists: your agents should never break because billing infrastructure hiccuped. If the cost can’t be figured out in the moment, MarginFront still keeps the event so you don’t lose history, and gives you a clean path to fix it later.

How to fix each non-PROCESSED state

Both fixes are one-time dashboard actions (or a single API call each). After you apply them, MarginFront retroactively recalculates cost for every affected event that was parked in that state. If the event came back as MISSING_VOLUME_DATA:
  • In the dashboard, go to the “Needs attention” section under Usage Events and fill in the missing token counts or quantity for the flagged events.
  • Or call POST /v1/events/fill-volume with the event ID and the correct volume numbers.
If the event came back as NEEDS_COST_BACKFILL:
  • In the dashboard, go to the “Needs attention” section under Usage Events. If the model is unrecognized, map it to a known one. If the model is recognized but missing cache-token pricing, email [email protected] to have cache pricing added.
  • For an unrecognized model, you can also call POST /v1/events/map-model with the model name and the service it should be priced as.
In both cases, once the fix is applied, every past event in that state auto-updates with a real cost, and future events using the same model (or the same well-formed payload shape) will be PROCESSED immediately. No manual step needed again. Do NOT retry events that were stored. They’re already in the database. Retrying would create duplicates. The fix happens on MarginFront’s side, not by resending the event.

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. A 200 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

An event gets this status for one of two reasons:
  • The model is unknown. The model + modelProvider you sent isn’t in MarginFront’s pricing table.
  • The model is known but has no cache pricing. The model is in the table, but it has no rate for cache tokens — and your event included cache tokens (cacheReadTokens or cacheWriteTokens). Rather than quietly pricing that cache at $0 and under-counting your cost, MarginFront flags the event.
The error message on the failed record tells you which case you hit. In both cases:
  1. The event is saved with cost = null. It is NOT lost.
  2. The response includes the event in results.failed with the code NEEDS_COST_BACKFILL and stored: true.
  3. In the MarginFront dashboard, go to the “Needs attention” section under Usage Events.
  4. If the model is unknown, map it to a known one — MarginFront creates a permanent mapping and retroactively calculates cost for every event that used that model. If the model is known but missing cache pricing, email [email protected] to have cache pricing added for it.
  5. Once resolved, future events with that model auto-resolve. No manual step needed again.
Do NOT retry events that have 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:
Or better yet, leave 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


Quick checklist

Before you ship, make sure:
  • The SDK is initialized with your secret key (mf_sk_*), not the publishable key
  • customerExternalId matches the external ID you set up for each customer
  • agentCode matches the agent code shown in the dashboard
  • signalName matches the signal name shown in the dashboard
  • For LLM events, you’re sending inputTokens and outputTokens from 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 fireAndForget is ON, which is the default)
  • You’re not retrying events that came back with stored: true