#AI#大模型#独立开发

AI Usage Event Metering Ledger: Idempotent Deduplication, Late Corrections, and Auditable Billing

Gateway retries, streaming segments, and vendor callbacks create duplicate or late usage. This article designs immutable usage events, unique dedup keys, conflict isolation, billing-period watermarks, and three-way reconciliation.

Key Points

  • Topic Focus: AI、大模型、独立开发
  • Suitable for: indie hackers, full-stack engineers, AI application developers
  • Core value: quickly understand background, practical paths, common pitfalls, and engineering advice
Author @admin·Published July 26, 2026·Updated July 26, 2026

AI usage isn't one request mapping to one tidy record. A single Agent run might call three models, hit a cache, retry a tool, and settle streaming; if the gateway's database write succeeds but the response is lost it will redeliver, and the queue might also deliver twice. If the consumer increments the customer's usage every time it receives a message, retries turn into double charges; if you keep only one record per request_id to avoid duplicates, you lose multiple legitimate segments within the same request.

AI Cost Attribution answers which user and feature a cost belongs to, and LLM Rate Limiting and Quota protects capacity before execution. This article only handles billable evidence: how many billable units actually occurred, how to prove the same fact is posted only once, and how late corrections don't rewrite history.

First define the minimal, stable metering event

The producer generates an event at the boundary where it can confirm actual usage. Model calls record the vendor usage, cache category, and success status; self-hosted inference records verified tokens, GPU time, or product-defined units; result-based billing is produced only by deterministic business-completion events. Estimated values can be used for reservation, but they can't quietly impersonate the final bill.

json
{
  "specversion": "1.0",
  "id": "evt_01J...",
  "type": "ai.usage.v2",
  "source": "llm-gateway/prod",
  "time": "2026-07-18T09:10:11Z",
  "subject": "account/ac_42",
  "data": {
    "meter": "model.output_token",
    "quantity": "842",
    "unit": "token",
    "operationId": "op_7f",
    "segment": 3,
    "providerRequestId": "req_vendor_9",
    "occurredAt": "2026-07-18T09:10:09Z",
    "schemaVersion": 2
  }
}

Quantities use integer minimal units or fixed-precision decimal strings, never floats. An event carries both occurrence time and reception time, and retains tenant, meter, operation, segment, and producer version. Raw prompts, user text, and unlimited tags don't enter the ledger; product plans and prices aren't copied in as facts either — metering and pricing are joined by version downstream.

The idempotency key represents a business fact, not a delivery

Producer retries must reuse the same event ID. Segment events use the stable tuple (operationId, meter, segment), and vendor events can bind a trusted provider request ID and usage kind. The database places a unique constraint on (source, id) and on the business idempotency key; same key with same payload hash returns the original receipt, same key with a different payload goes into CONFLICT isolation — the later one must never overwrite.

sql
INSERT INTO usage_events(id, source, idempotency_key, payload_hash, payload)
VALUES ($id, $source, $key, $hash, $json)
ON CONFLICT (source, idempotency_key) DO NOTHING;

payload_hash is computed over the canonical bytes defined by the schema version, unaffected by JSON field order. When the inserted row count is zero, read the existing event: same hash returns the original receipt; different means atomically write a conflict record and alert — you can't just silently swallow it with DO NOTHING. Inserting, updating the ingest cursor, and writing to the outbox must be in the same transaction. Message systems provide at-least-once delivery; the ledger gets once-billing-effect through the unique constraint — don't claim end-to-end 'exactly once' over the network. For event-driven delivery and replay boundaries, see Event-Driven Architecture.

The ledger is append-only; errors are corrected via reverse events

A POSTED quantity, account, and occurredAt are never UPDATEd in place. If you discover a tokenizer version error or the vendor's final usage differs, create a negative reversal, then append the correct event. Correction commands use stable IDs and place a unique constraint on (originalEventId, correctionRevision); the transaction locks the correction chain and validates the previous revision, so two operators can't reverse the same version twice at the same time. For subsequent corrections, append the next revision against the current effective version instead of repeatedly reversing the original event.

Correction events retain correctionOf, discovery time, reason, and operator, and distinguish the servicePeriod inherited from the original event from the postingPeriod that's actually adjusted. An open billing period can return to its original draft; a closed billing period can only be associated with a credit note, a next-period adjustment, or a manual case — you can't resend the old occurredAt as an ordinary new usage. Deleting or overwriting the original record makes invoices, audits, and user exports impossible to rebuild.

States should at minimum distinguish RECEIVED, VALIDATED, POSTED, CONFLICT, and REJECTED. Unsupported schema, unknown units, missing accounts, or out-of-range quantities all go into a quarantine queue and aren't automatically treated as zero. After fixing the parser, replay from the original payload, still using the original event ID, to avoid the fix action itself being metered again.

Late events need clear billing-period watermarks

Each meter defines an allowed late window, billing-period cutoff, and closing rules. Within the window, events enter the current draft by occurredAt; events arriving after an invoice is closed must not silently rewrite the issued bill, but instead generate a next-period adjustment, credit note, or manual review per the contract. The user interface must distinguish 'current estimate', 'settled', and 'late adjustment' — numbers must not jump without explanation.

When exporting to Stripe or another billing system, use the ledger event ID as a stable external identifier, but don't assume the vendor deduplicates forever. Delivery records the provider's idempotency scope, retention period, attempt, request hash, object ID, and response; on timeout, first mark it UNKNOWN and confirm the result through a query or bill reconciliation. Only replay unconfirmed outbox entries within the guarantee window; past the deadline, freeze and handle manually — never blindly resend. Quota reservations and the actual ledger share the operation ID, but the admission rejection in AI Platform Tenant Quota is not the same as actual consumption.

Acceptance with fault injection and three-way reconciliation

Test delivering the same event a hundred times, same key with different payloads, out-of-order segments, the producer crashing after writing to the database, concurrent corrections, cross-period lateness, and exporter timeouts crossing the idempotency window. Assert that the ledger net quantity, external metering, and user detail all agree, and that conflicts are never automatically billed. Compare the gateway's raw usage, the internal POSTED net, and the accepted net from the billing vendor by account and meter; if the difference exceeds an absolute/relative threshold, freeze that meter's billing.

On release, first shadow-write the ledger without pushing invoices, replay a full billing period, and compare against the old aggregate, then switch per meter. On rollback, stop the exporter, restore the old consumer version and cursor, but keep the new ledger events; fix wrong events with reversals — don't drop tables or reset unique keys. Reliable metering isn't making messages never duplicate — it's making any duplication, lateness, and correction converge on the same rebuildable net result.

Further reading

Related

Related Content