Privacy
Receipts stay on your machine. Publishing a review is off by default, opt-in per spend, and sends exactly one record per call you labelled — every field of which is already public on chain, or is metadata about a payment you chose to publish.
Off unless you turn it on
x402-spend writes receipts to a local SQLite file and does nothing else with them. There is no account, no telemetry, and no endpoint it talks to unless you name one.
Review posting is configured per spend, not globally, so turning it on for one agent does not turn it on for another:
const spend = createSpend(client, new SqliteSpendStore(), {
review: { endpoint: "https://api.x402spend.dev/v1/reviews" },
});
Without that review block, nothing ever leaves the machine.
What leaves, when it is on
One record per labelled call. An unlabelled receipt has no verdict to publish, and a receipt with no settlement — a refused offer, a transport failure — has nothing checkable in it, so neither is ever sent.
The record is exactly these fields and nothing else:
| Field | What it is |
|---|---|
resourceUrl | The endpoint you paid, with the query string and fragment stripped |
taskClass | Your label for the kind of work — only if it is shared vocabulary |
network asset amount payTo | What was paid, in what, to whom |
transaction payer | The settlement, and the address that paid it |
outcome reason recovery note | Your verdict, and your own words about it |
paidMs ts | How long the payment leg took, and when |
Every one of network, asset, amount, payTo, transaction and payer is already public on chain. Publishing them tells the world nothing the chain was not already telling it; what the review adds is the link between that payment and a verdict about what it bought.
What never leaves
The receipt holds more than the review publishes, and the rest stays local: the per-leg transport timings, byte counts, the HTTP status, the request method, how many payment alternatives were offered, and the local receipt id.
What is never recorded at all
Nothing about what you asked for or what came back is in the receipt in the first place — not the request body, not the response body, not headers. The client records that a call happened, what it cost and how long it took. It never sees the content, so no setting can leak it.
The query string and fragment are stripped from resourceUrl before it leaves, because API keys live in query strings.
Your own words
Two fields are free text you wrote rather than something the protocol already made public, and they are treated differently from each other.
taskClass is published only when it is shared vocabulary. One of geocode_forward, geocode_reverse, search_known, search_relevance, weather or price. A class of your own invention stays on the machine — it is still written to your local receipt, and still groups your own reports; it simply is not published. label() returns taskClassWithheld: true when that happens, so you find out at the call rather than from an empty column weeks later.
This matters because taskClass is set at the call site and often built from a variable, where taskClass: `lookup-for-${customerId}` is a natural thing to write and an unintended thing to publish. Little is lost by withholding it: a published dataset is worth something because values aggregate across callers, and a string only you use aggregates with nothing.
To publish one deliberately:
review: { endpoint: "…", postCustomTaskClass: true }
`note` is published verbatim
note is not filtered, because unlike a task class it is a finding about the call rather than a key to group by, and a closed set of findings would be useless. Keep it about the response — wrong city, three days stale — and never put a customer name, an internal identifier, a prompt or a key in it.
This is a guarantee about this client, not about the API
The filtering happens in x402-spend before anything is sent. The server accepts any taskClass up to 64 characters from any client, because it cannot know what vocabulary a third-party client uses. If you are writing your own client against the API, this rule is yours to implement.
The payer address is published
This is disclosed rather than buried, because it is the part people are most likely to be surprised by: a published review names the address that paid.
It has to. The whole claim this project makes is that a review is anchored to a payment that actually happened — and the only way anyone can check that for themselves is to look up the settlement and see that the payer paid the recipient the stated amount. Remove the payer and the review becomes exactly the unverifiable assertion it exists to replace.
If an address is one you do not want associated with a published opinion, use a different address for calls you intend to review, or leave review posting off.
If any of this changes
It changes visibly. The review wire format carries a schema number — it is 2 today, and it went to 2 precisely so that a change in what the data means could be read off the data itself rather than inferred from the date it was collected. A change to what is published means a release note and a version bump, not a quiet edit to this page.
Published reviews are licensed CC BY 4.0; attribute x402-spend-reviews.