> ## Documentation Index
> Fetch the complete documentation index at: https://docs.marginfront.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Tracking Usage Events

> Where to put the SDK code in your agent, what fields to send, and real-world examples

# 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

```bash theme={null}
npm install @marginfront/sdk
```

## Initialize the client

```typescript theme={null}
import { MarginFrontClient } from "@marginfront/sdk";

// Your secret API key from the MarginFront dashboard (Build > API keys).
// NEVER put the actual key in your code -- use an environment variable.
const mf = new MarginFrontClient(process.env.MF_API_SECRET_KEY);
```

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

| Field                | Type   | What it is                                                                                                                                                                                                                                                                      |
| -------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `customerExternalId` | string | Your customer's ID in **your** system. Whatever you use to identify them. If MarginFront has not seen this ID before, the customer is created automatically.                                                                                                                    |
| `agentCode`          | string | A stable identifier for the agent or product that did the work. If MarginFront has not seen this code before, the agent is created automatically.                                                                                                                               |
| `signalName`         | string | The billing unit being tracked (e.g. `messages`, `sms-sent`, `report-pages`). If MarginFront has not seen this name before, the signal is created automatically.                                                                                                                |
| `model`              | string | The model or service that did the work. Pass whatever your provider returns. Examples: `"gpt-4o"`, `"claude-sonnet-4"`, `"twilio-sms"`. To discover the canonical names MarginFront recognizes, call `mf.services.list({ provider: '<your-provider>' })` (added in SDK 0.12.0). |
| `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`  | integer | The number of prompt tokens (what you sent to the model). Must be a whole integer, 0 or greater.   |
| `outputTokens` | integer | The number of completion tokens (what the model sent back). Must be a whole integer, 0 or greater. |

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.

| Field              | Type    | What it is                                                                                                                                                                                             |
| ------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `cacheReadTokens`  | integer | Prompt-cache read tokens (cache hits). Read it from your provider response (Anthropic `cache_read_input_tokens`, OpenAI `prompt_tokens_details.cached_tokens`). Priced at the cheaper cache-read rate. |
| `cacheWriteTokens` | integer | Prompt-cache write tokens (cache creation). Read it from your provider response (Anthropic `cache_creation_input_tokens`). Priced at the cache-write rate.                                             |

### For variable-quantity billing

| Field      | Type   | Default | What it is                                                                                                                                                                                                                           |
| ---------- | ------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `quantity` | number | `1`     | How many units of work happened. Whole numbers or fractions both work. Use this for per-page, per-minute, per-SMS billing. Tracking a fractional unit like 14.137 seconds of compute? Send it exactly as it is — no rounding needed. |

### 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.                                      |
| `environment` | string (one of `production`, `staging`, `development`, `testing`) | —       | Where the event happened. Lets MarginFront split your AI spend into Cost of Goods Sold vs Research & Development. See [Tagging events for cost classification](#tagging-events-for-cost-classification). 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:

```typescript theme={null}
await mf.usage.record({
  customerExternalId: "acme-001",
  agentCode: "cs-bot-v2",
  signalName: "messages",
  model: "gpt-4o",
  modelProvider: "openai",
  inputTokens: 523,
  outputTokens: 117,
  environment: "production", // "production" | "staging" | "development" | "testing"
});
```

The first time a signal sees an event that carries an `environment`, MarginFront sorts that signal into a cost category for you:

| `environment` you send | Cost category it becomes                                    |
| ---------------------- | ----------------------------------------------------------- |
| `production`           | Production — Cost of Goods Sold                             |
| `development`          | Development — Research & Development                        |
| `testing`              | Development — Research & Development                        |
| `staging`              | Left unset on purpose — choose it yourself in the dashboard |

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.

```typescript theme={null}
import OpenAI from "openai";
import { MarginFrontClient } from "@marginfront/sdk";

const openai = new OpenAI();
const mf = new MarginFrontClient(process.env.MF_API_SECRET_KEY);

async function handleCustomerQuestion(customerId: string, question: string) {
  // Step 1: Call OpenAI like you normally would
  const response = await openai.chat.completions.create({
    model: "gpt-4o",
    messages: [
      { role: "system", content: "You are a helpful customer support agent." },
      { role: "user", content: question },
    ],
  });

  // Step 2: Get the answer to send back to the customer
  const answer = response.choices[0].message.content;

  // Step 3: Tell MarginFront what just happened.
  //
  // This runs in the background by default (fireAndForget: true).
  // If the network is down, it retries automatically.
  // If MarginFront is unreachable, your agent keeps running -- the customer
  // still gets their answer. Billing is important, but never more important
  // than your core product working.
  try {
    await mf.usage.record({
      customerExternalId: customerId, // your customer's ID in your system
      agentCode: "cs-bot-v2", // the agent code from the dashboard
      signalName: "messages", // the metric you're tracking
      model: response.model, // "gpt-4o" -- straight from OpenAI's response
      modelProvider: "openai", // always lowercase
      inputTokens: response.usage.prompt_tokens, // how many tokens the prompt used
      outputTokens: response.usage.completion_tokens, // how many tokens the answer used
    });
  } catch (error) {
    // With fireAndForget ON (the default), this catch block almost never runs.
    // The SDK handles retries internally. This is just a safety net.
    console.error("MarginFront tracking failed (non-blocking):", error);
  }

  return answer;
}
```

**Key mapping from OpenAI's response to MarginFront fields:**

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

```typescript theme={null}
import twilio from "twilio";
import { MarginFrontClient } from "@marginfront/sdk";

const twilioClient = twilio(
  process.env.TWILIO_SID,
  process.env.TWILIO_AUTH_TOKEN,
);
const mf = new MarginFrontClient(process.env.MF_API_SECRET_KEY);

async function sendSmsForCustomer(
  customerId: string,
  to: string,
  body: string,
) {
  // Step 1: Send the SMS through Twilio
  const message = await twilioClient.messages.create({
    to,
    from: process.env.TWILIO_PHONE_NUMBER,
    body,
  });

  // Step 2: Twilio confirmed it was sent -- now tell MarginFront.
  //
  // quantity is 1 here (one SMS). You could omit it since 1 is the default,
  // but being explicit makes the code easier to read later.
  try {
    await mf.usage.record({
      customerExternalId: customerId,
      agentCode: "notification-agent",
      signalName: "sms-sent",
      model: "sms-send", // not an LLM model -- just a label for the service
      modelProvider: "twilio", // which provider handled it
      quantity: 1, // one SMS sent
    });
  } catch (error) {
    console.error("MarginFront tracking failed (non-blocking):", error);
  }

  return message.sid;
}
```

**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](/concepts#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.

```typescript theme={null}
import Anthropic from "@anthropic-ai/sdk";
import { MarginFrontClient } from "@marginfront/sdk";

const anthropic = new Anthropic();
const mf = new MarginFrontClient(process.env.MF_API_SECRET_KEY);

async function generateReport(customerId: string, topic: string) {
  // Step 1: Generate the report with Claude
  const response = await anthropic.messages.create({
    model: "claude-sonnet-4-20250514",
    max_tokens: 8000,
    messages: [
      { role: "user", content: `Write a market research report on: ${topic}` },
    ],
  });

  const reportText =
    response.content[0].type === "text" ? response.content[0].text : "";

  // Step 2: Figure out how many pages the report is.
  // (Your real logic might be more sophisticated -- this is just an example.)
  const estimated_pages = Math.ceil(reportText.length / 3000);

  // Step 3: Tell MarginFront about the report.
  //
  // quantity = number of pages. This is what the customer gets billed for.
  // inputTokens / outputTokens = LLM usage. This tracks your cost from Anthropic.
  // Both matter, but for different reasons.
  try {
    await mf.usage.record({
      customerExternalId: customerId,
      agentCode: "research-agent",
      signalName: "report-pages", // the billable metric is pages
      model: response.model, // "claude-sonnet-4-20250514"
      modelProvider: "anthropic",
      inputTokens: response.usage.input_tokens, // Anthropic uses input_tokens (not prompt_tokens)
      outputTokens: response.usage.output_tokens, // Anthropic uses output_tokens (not completion_tokens)
      quantity: estimated_pages, // 15 pages = 15 units billed
    });
  } catch (error) {
    console.error("MarginFront tracking failed (non-blocking):", error);
  }

  return { reportText, pages: estimated_pages };
}
```

**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.

```typescript theme={null}
await mf.usage.record({
  customerExternalId: "acme-001",
  agentCode: "cs-bot-v2",
  signalName: "messages",
  model: "gpt-4o",
  modelProvider: "openai",
  inputTokens: 812,
  outputTokens: 245,

  // metadata is free-form. Put whatever is useful for YOUR debugging and analytics.
  // MarginFront stores it with the event but does NOT use it for billing or
  // cost calculation. It will not appear on invoices.
  metadata: {
    conversationId: "conv_abc123", // link this event back to a chat thread
    promptTemplate: "support-v3.2", // which prompt version generated this
    abTestVariant: "concise-responses", // for your own A/B test analysis
    customerTier: "enterprise", // useful for segmenting cost reports
    responseLatencyMs: 1243, // track performance alongside cost
  },
});
```

**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.

```typescript theme={null}
import Anthropic from "@anthropic-ai/sdk";
import { MarginFrontClient } from "@marginfront/sdk";

const anthropic = new Anthropic();
const mf = new MarginFrontClient(process.env.MF_API_SECRET_KEY);

async function sendColdOutreach(customerId: string, prospectQuery: string) {
  // Step 1: Find the prospect via Exa
  const exaResults = await exa.search({ query: prospectQuery, type: "people" });
  const prospect = exaResults.results[0];

  // Step 2: Enrich with verified email + role via Hunter.io
  const enrichment = await hunter.enrich({
    domain: prospect.domain,
    name: prospect.name,
  });

  // Step 3: Generate the personalized message with Claude Opus
  const llmResponse = await anthropic.messages.create({
    model: "claude-opus-4-1",
    max_tokens: 1500,
    messages: [
      {
        role: "user",
        content: `Write a personalized cold outreach to ${enrichment.name} at ${enrichment.company}, role: ${enrichment.role}.`,
      },
    ],
  });
  const message =
    llmResponse.content[0].type === "text" ? llmResponse.content[0].text : "";

  // Step 4: Send via Pipedream workflow
  await pipedream.invoke("send-cold-email", {
    to: enrichment.email,
    body: message,
  });

  // Step 5: Tell MarginFront about the WHOLE outreach (one event, four services)
  try {
    await mf.usage.record({
      customerExternalId: customerId,
      agentCode: "outreach-bot",
      signalName: "outreaches-sent",
      // Top-level quantity stays signal-level: ONE outreach to ONE prospect.
      // Per-service volume lives inside each services[] entry.
      quantity: 1,
      services: [
        {
          // Exa people-search API call
          model: "exa-search",
          modelProvider: "exa",
          quantity: 1, // 1 search call
        },
        {
          // Hunter.io enrichment call
          model: "hunter-enrich",
          modelProvider: "hunter",
          quantity: 1, // 1 enrichment call
        },
        {
          // Claude Opus writing the message. Track tokens AND the call count.
          model: llmResponse.model,
          modelProvider: "anthropic",
          inputTokens: llmResponse.usage.input_tokens,
          outputTokens: llmResponse.usage.output_tokens,
          quantity: 1, // 1 LLM call
        },
        {
          // Pipedream workflow firing the email
          model: "pipedream-workflow",
          modelProvider: "pipedream",
          quantity: 1, // 1 send
        },
      ],
    });
  } catch (error) {
    console.error("MarginFront tracking failed (non-blocking):", error);
  }

  return message;
}
```

**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

| Shape          | When to use                                                       | Top-level fields                              | `services[]`                     |
| -------------- | ----------------------------------------------------------------- | --------------------------------------------- | -------------------------------- |
| Single-service | One event, one underlying service. The 90% case for chatbots.     | `model` + `modelProvider` + volume (required) | omit                             |
| Multi-service  | One event, multiple underlying services for one business outcome. | `quantity` is signal-level (default 1)        | one entry per service (required) |

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

```typescript theme={null}
services: [
  {
    model: "claude-opus-4-1",
    modelProvider: "anthropic",
    inputTokens: 4500,
    outputTokens: 800,
    quantity: 1,
  },
  {
    model: "claude-opus-4-1",
    modelProvider: "anthropic",
    inputTokens: 1200,
    outputTokens: 400,
    quantity: 1,
  },
];
```

**Option B: aggregate into one `services[]` entry with summed tokens and `quantity` = call count.** Cleaner if you don't need per-call resolution.

```typescript theme={null}
services: [
  {
    model: "claude-opus-4-1",
    modelProvider: "anthropic",
    inputTokens: 5700, // 4500 + 1200
    outputTokens: 1200, // 800 + 400
    quantity: 2, // 2 LLM calls aggregated
  },
];
```

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](/concepts#one-event-multiple-services) for the conceptual mental model and [Usage Events API reference](/api-reference/usage-events) 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.

```typescript theme={null}
const response = await mf.usage.recordBatch([
  // LLM event
  {
    customerExternalId: "acme-001",
    agentCode: "cs-bot-v2",
    signalName: "messages",
    model: "gpt-4o",
    modelProvider: "openai",
    inputTokens: 523,
    outputTokens: 117,
  },
  // Non-LLM event (different customer, different agent)
  {
    customerExternalId: "beta-corp",
    agentCode: "notification-agent",
    signalName: "sms-sent",
    model: "sms-send",
    modelProvider: "twilio",
    quantity: 3,
  },
  // Another LLM event with metadata
  {
    customerExternalId: "acme-001",
    agentCode: "research-agent",
    signalName: "report-pages",
    model: "claude-sonnet-4-20250514",
    modelProvider: "anthropic",
    inputTokens: 4000,
    outputTokens: 6500,
    quantity: 12,
    metadata: { reportTopic: "Q2 market trends" },
  },
]);
```

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:

```typescript theme={null}
const response = await mf.usage.recordBatch(records);

// Check if any events in the batch had problems
if (response.failed > 0) {
  console.warn(
    `${response.failed} of ${response.processed} events had issues:`,
  );

  for (const failure of response.results.failed) {
    // "error" tells you what went wrong in plain English
    console.warn(`  - ${failure.error}`);

    // "record" echoes back the original data so you can identify which event failed
    console.warn(`    Record:`, failure.record);
  }
}

// The rest of the batch still succeeded -- you don't need to resend the whole thing
console.log(`${response.successful} events recorded successfully`);
```

***

## 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:

```typescript theme={null}
const mf = new MarginFrontClient(process.env.MF_API_SECRET_KEY, {
  fireAndForget: false, // Now usage.record() can throw errors
});

try {
  await mf.usage.record({
    customerExternalId: "acme-001",
    agentCode: "cs-bot-v2",
    signalName: "messages",
    model: "gpt-4o",
    modelProvider: "openai",
    inputTokens: 100,
    outputTokens: 50,
  });
} catch (error) {
  // With fireAndForget OFF, this catch block WILL run on network errors.
  // You're responsible for retrying or logging.
  console.error("Failed to record usage event:", error);
}
```

**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

| 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`      | integer | Number of prompt/input tokens. Must be a whole integer, 0 or greater. Required for MarginFront to calculate LLM cost.                                                                                           |
| `outputTokens`     | integer | Number of completion/output tokens. Must be a whole integer, 0 or greater. Required for MarginFront to calculate LLM cost.                                                                                      |
| `cacheReadTokens`  | integer | Optional. Prompt-cache read tokens (cache hits). Priced at the cheaper cache-read rate. Read it from the provider response (Anthropic `cache_read_input_tokens`, OpenAI `prompt_tokens_details.cached_tokens`). |
| `cacheWriteTokens` | integer | Optional. Prompt-cache write tokens (cache creation). Priced at the cache-write rate. Read it from the provider response (Anthropic `cache_creation_input_tokens`).                                             |

### For variable-quantity billing

| Field      | Type   | Default | Description                                                                                                                                                                                                                                       |
| ---------- | ------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `quantity` | number | `1`     | Number of billing units, 0 or greater. Whole numbers or fractions both work. Use for per-page, per-minute, per-image, per-SMS billing. For fractional units (e.g. 14.137 seconds of compute), send them exactly as they are — no rounding needed. |

### 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.                                                                                  |
| `environment` | string                    | —       | Where the event happened: `production`, `staging`, `development`, or `testing`. Used to split spend into Cost of Goods Sold vs Research & Development. 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

| State                 | Event saved? | Cost calculated?    | What it means                                                                                                                                                                                                                                                                                                                                          |
| --------------------- | ------------ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `PROCESSED`           | Yes          | Yes                 | The event was received, the model was recognized, and the cost was calculated. Nothing for you to do.                                                                                                                                                                                                                                                  |
| `MISSING_VOLUME_DATA` | Yes          | No (cost is `null`) | The event was received, but it didn't include the token counts or quantity needed to price it. Cost stays blank until you backfill the missing numbers.                                                                                                                                                                                                |
| `NEEDS_COST_BACKFILL` | Yes          | No (cost is `null`) | The event was received, but cost couldn't be set. Either the `model` + `modelProvider` you sent isn't in MarginFront's pricing table yet, or it is in the table but has no rate for the cache tokens your event reported (so MarginFront won't quietly price that cache at \$0). Cost stays blank until the model is mapped or cache pricing is added. |

**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 `team@marginfront.com` 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:

```typescript theme={null}
const response = await mf.usage.record({
  customerExternalId: "acme-001",
  agentCode: "cs-bot-v2",
  signalName: "messages",
  model: "my-custom-model",
  modelProvider: "custom",
  inputTokens: 500,
  outputTokens: 200,
});

// For single events, the response still tells you if there was a problem
if (response.failed > 0) {
  for (const failure of response.results.failed) {
    console.warn("Event issue:", failure.error);
  }
}
```

### 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 `team@marginfront.com` 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:

```typescript theme={null}
// Your core product logic -- this MUST work
const answer = await openai.chat.completions.create({ ... });

// Billing -- important but never more important than the answer
try {
  await mf.usage.record({ ... });
} catch (error) {
  // Log it. Investigate later. The customer got their answer.
  console.error('Usage tracking failed:', error);
}
```

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

| Code                  | Event saved? | What happened                                                                                                                                   | What to do                                                                                                                                                                                                          |
| --------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NEEDS_COST_BACKFILL` | Yes          | The model+provider isn't in the pricing table, OR it is but has no rate for the cache tokens your event reported. Event saved with cost = null. | Do NOT retry. For an unknown model, map it in the dashboard or call `POST /v1/events/map-model`. For a known model missing cache pricing, email `team@marginfront.com`. Cost backfills automatically once resolved. |
| `MISSING_VOLUME_DATA` | Yes          | The event is missing token counts or quantity needed to price it. Event saved with cost = null.                                                 | Do NOT retry. Fill in the missing numbers in the dashboard or call `POST /v1/events/fill-volume`. 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
* [ ] `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`
