What it sends
MarginFuse needs to know what a request cost and who it was for. It does not need to know what was in it, so the wire format has nowhere to put that.
Every field, all of them
This list is read from the API contract rather than written here, so it cannot fall behind what the server actually accepts. There are 18 fields and this is all of them.
- eventId
- customerId
- feature
- provider
- model
- requestedModel
- costUsd
- occurredAt
- outcome
- decisionId
- retryOfEventId
- correctsEventId
- usage.inputTokens
- usage.outputTokens
- usage.cachedInputTokens
- usage.cacheCreationTokens
- usage.images
- usage.audioSeconds
How that claim is kept honest
A promise about privacy is worth what its enforcement is worth, so this one is checked in three places that all fail loudly.
- The server rejects unknown fields. Every request schema is strict, so an extra key is a 422 rather than something quietly stored.
- The contract is tested for it. A test walks the generated API document and fails the build if a field appears whose name could carry content.
- The conformance suite checks the bytes. Every scenario every SDK runs scans the request bodies that actually left the process, not the types that were supposed to shape them. An SDK that somehow serialized a prompt would fail before it could be published.
About the identifiers
customerId is whatever you pass: your own id, or the Stripe customer id. It is the join key between cost and revenue, so it has to be stable, but it does not have to be meaningful. If your ids are themselves sensitive, pass an opaque id and map it on your side.
feature is a short key you choose, like ai_chat. It is what makes cost breakable down by product surface, so keep it stable and low cardinality: it is a label, not a request id.
The Stripe connection
Revenue comes from a read-only Stripe connection. MarginFuse reads subscriptions, prices and invoices. It cannot create a charge, change a plan, or modify a customer, because the connection is not granted the ability to.