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

npm install @marginfront/sdk

Initialize the client

import { MarginFrontClient } from "@marginfront/sdk";

// Your secret API key from the MarginFront dashboard (Settings > 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

FieldTypeWhat it is
customerExternalIdstringYour customer’s ID in your system. Whatever you use to identify them — "acme-001", "cust_abc123", etc.
agentCodestringThe 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".
signalNamestringThe metric being tracked. Matches the signal you set up in the dashboard. Examples: "messages", "sms-sent", "report-pages".
modelstringThe model or service that did the work. Pass whatever your provider returns. Examples: "gpt-4o", "claude-sonnet-4", "twilio-sms".
modelProviderstringThe provider name, always lowercase. This tells MarginFront which pricing table to look in. Examples: "openai", "anthropic", "twilio", "google", "aws".
FieldTypeWhat it is
inputTokensnumberThe number of prompt tokens (what you sent to the model). Must be 0 or greater.
outputTokensnumberThe number of completion tokens (what the model sent back). Must be 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.

For variable-quantity billing

FieldTypeDefaultWhat it is
quantitynumber1How 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

FieldTypeDefaultWhat it is
usageDatestring (ISO 8601) or DatenowWhen the work actually happened. Only needed if you’re back-filling historical events. Example: "2026-04-10T14:30:00Z".
metadataobject{}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.
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 fieldMarginFront field
response.modelmodel
response.usage.prompt_tokensinputTokens
response.usage.completion_tokensoutputTokens

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

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

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

FieldTypeDescription
customerExternalIdstringYour customer’s ID in your system. Must match the externalId you set when creating the customer in MarginFront (or via auto-provisioning).
agentCodestringThe agent code from the MarginFront dashboard. Identifies which product/agent did the work.
signalNamestringThe name of the metric being tracked. Matches the signal you configured in the dashboard.
modelstringThe model or service identifier. Pass whatever your provider returns (e.g., response.model from OpenAI). Case-insensitive.
modelProviderstringThe provider name, always lowercase. Tells MarginFront which pricing table to look up.
FieldTypeDescription
inputTokensnumberNumber of prompt/input tokens. Must be >= 0. Required for MarginFront to calculate LLM cost.
outputTokensnumberNumber of completion/output tokens. Must be >= 0. Required for MarginFront to calculate LLM cost.

For variable-quantity billing

FieldTypeDefaultDescription
quantitynumber1Number of billing units. Use for per-page, per-minute, per-image, per-SMS billing. Must be >= 0.

Optional

FieldTypeDefaultDescription
usageDatestring (ISO 8601) or DatenowWhen the work happened. Use for back-filling historical events.
metadataobject{}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. A 200 tells you the request was received. The response body tells you what actually happened. Always check failed > 0 in the response:
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

If you send a model + modelProvider combination that MarginFront doesn’t recognize (not in the pricing table), this happens:
  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. Map the unrecognized model to a known one. MarginFront creates a permanent mapping and retroactively calculates cost for every event that used that model.
  5. 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:
// 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

CodeEvent saved?What happenedWhat to do
NEEDS_COST_BACKFILLYesThe 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_ERRORNoSomething broke on MarginFront’s side.Safe to retry.
VALIDATION_ERRORNoA 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