docs(x402): state the settle success criterion per billing model - #369
Open
r-marques wants to merge 5 commits into
Open
docs(x402): state the settle success criterion per billing model#369r-marques wants to merge 5 commits into
r-marques wants to merge 5 commits into
Conversation
On a pay-as-you-go plan a fully successful `POST /api/v1/x402/settle` returns
`creditsRedeemed: "0"` and `remainingBalance: "0"`. Those plans hold no credit
balance — each settle is one direct charge — and the charge is referenced by
`orderTx` (fiat rails) or `transaction` (crypto rails).
Several pages here published the opposite advice. The payments skill's
`SKILL.md` said "Proof of purchase = `success: true` with `creditsRedeemed` > 0
and a `remainingBalance`", and `references/autonomous-operations.md` and
`references/client-integration.md` said the same. That check is unreachable on a
pay-as-you-go plan, so it reports a real charge as a decline — and on a card rail
that invites a retry of a payment that already went through, which is the one
thing we tell integrators never to do.
Every affected page now states the criterion per billing model, using the
discriminator the API has always returned:
- `credits` — `success === true` and `creditsRedeemed > 0`.
- `pay-as-you-go` — `success === true` plus a non-empty `orderTx` (fiat) /
`transaction` (crypto); the credit fields are always `"0"` and carry no
information.
- The values are strings, so `"0"` is truthy while `Number("0") > 0` is false.
Changed: the payments skill + three of its references, the five IDE rule files
that list the settle response fields, both x402 specs (receipt tables plus a new
"Was the buyer charged?" section), `products/x402-facilitator/how-it-works.mdx`,
`agents-guide/check-credits.mdx`, `solutions/api-providers.mdx`,
`integrate/patterns/charge-credits.mdx` and
`development-guide/nevermined-x402.mdx`.
`api-reference/openapi.json` is hand-maintained, and its
`SettlePermissionResponse` schema did not match the endpoint at all — it named
`permissionHash` / `creditsSettled` / `remainingCredits` / `txHash`, three of
which the API does not return, and typed the credit fields as integers. It now
describes the real response, including `billingModel` and the criterion. This is
the published, interactive API reference and clients are generated from it.
`integrations/youdotcom.mdx` already carried the correct guidance and needed no
change. No API shape changed; `billingModel` was already on the wire.
Refs nevermined-io/nvm-monorepo#2999
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVpimo1dBXnTJo9tQSy8SA
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Automations to automatically generate PRs for you. |
…tion Drive-by, inside the receipt-construction block the previous commit already edited to pass `billingModel` through. The example read `settlement.txHash` (TS) / `settlement.tx_hash` (Python), a field that exists on neither SDK's settle result: both return `transaction`, alongside `success`, `errorReason`, `payer`, `network`, `creditsRedeemed`, `remainingBalance` and `orderTx`. Copied as written it puts `undefined` / raises `AttributeError` into the receipt the agent hands its buyer. Verified against `origin/main` in both SDK repos, not against this branch. The same mistake appears in four other places that are not part of this change — `products/x402-facilitator/how-it-works.mdx` (two code blocks), `integrate/add-to-your-agent/express.mdx` and `skills/nevermined-payments/references/express-integration.md` — and is left for a separate PR rather than widened into here. Refs nevermined-io/nvm-monorepo#2999 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVpimo1dBXnTJo9tQSy8SA
…transaction Completes the previous commit, which fixed only the two occurrences inside the block it was already editing. Counting them showed six in total, all the same verified error, so the rest are folded in rather than split into a second review round. - products/x402-facilitator/how-it-works.mdx (TS + Python receipt construction) - integrate/add-to-your-agent/express.mdx (onAfterSettle hook) - skills/nevermined-payments/references/express-integration.md (same hook) The two Express ones are `onAfterSettle` callbacks, so the object is whatever the middleware passes rather than a direct settle return — checked rather than assumed. Both rails are safe: the x402 branch passes the resolved value of `settlePermissions`, i.e. a `SettlePermissionsResult`, and the MPP branch passes `MppSettleResult`, which extends it. `transaction` exists on both; `txHash` appears nowhere in the SDK's x402 or MPP surface, so the examples logged `undefined`. The express skill reference is why this could not wait: it ships to AI coding assistants over the same four routes as the skill itself, so leaving it kept handing agent builders a field that exists on neither SDK. As before, only the VALUE changes — `transactionHash` stays as the integrator's own key name in the receipt they build, since renaming that would change what their buyers parse. Refs nevermined-io/nvm-monorepo#2999 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVpimo1dBXnTJo9tQSy8SA
r-marques
marked this pull request as ready for review
September 8, 2026 14:11
Member
|
No description provided. |
Review nit on nevermined-io/payments#432. The criterion said what to do for `credits` and for `pay-as-you-go`, and the field doc claimed `billingModel` is "present regardless of `success`" — true of a current Nevermined API, false of one that predates the discriminator, where it does not arrive at all. All three fields are optional on the type precisely because of that case, and it was nowhere stated. The criterion now carries the third branch: an absent discriminator means an older deployment, apply the `credits` rule, and never read a missing `billingModel` as pay-as-you-go. It also names what the reviewer's ask did not cover — if `creditsRedeemed` is absent as well, the credits rule has nothing to evaluate, so `success` alone is the answer. Without that clause the documented snippet returns false for a settle that succeeded, which is the same decline-on-success bug this work exists to remove. The "present regardless of success" line is rephrased so both hold: reported whether or not the settle succeeded, and absent entirely on an older deployment. Wording is identical across both SDKs and the docs site. Refs nevermined-io/nvm-monorepo#2999 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVpimo1dBXnTJo9tQSy8SA
…footnote The footnote's tail described a deployment that has not existed since March. Two dates from `git log -S` on nvm-monorepo's `card-delegation-scheme.handler.ts`: `creditsRedeemed` has been in the settle response since 2026-03-25 (3744bc388), `billingModel` only since 2026-08-06 (66ed3614f) — four and a half months apart. So a deployment old enough to omit the discriminator still returns the credit fields, and the "if `creditsRedeemed` is absent too" clause can never fire. An unreachable conditional is worse than no conditional in a doc whose whole job is telling a reader which check to run: it is the part they stop and think about. The valuable half is unchanged and stays everywhere — an absent `billingModel` means an older deployment, apply the `credits` rule, and never read a missing discriminator as pay-as-you-go. Refs nevermined-io/nvm-monorepo#2999 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVpimo1dBXnTJo9tQSy8SA
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem, in plain terms
On a pay-as-you-go plan, a settle that charged the buyer $1.00 comes back like this:
{ "success": true, "network": "stripe", "billingModel": "pay-as-you-go", "creditsRedeemed": "0", "remainingBalance": "0", "orderTx": "pi_3U6tgrBYvSRKcV421ehH4bnX" }Those plans hold no credit balance — each settle is one direct charge, referenced by
orderTx(fiat rails) ortransaction(crypto rails). So the success check a credits integration already has,success === true && creditsRedeemed > 0, is unreachable there: it reports a real charge as a decline. On a card rail that invites a retry of a payment that already went through, and repeated attempts feed issuer fraud scoring.Several pages here published exactly that wrong check. The payments skill's
SKILL.mdsaid:and
references/autonomous-operations.mdandreferences/client-integration.mdsaid the same. Those files are shipped to AI coding assistants through four distribution routes, so the wrong criterion was being handed to agents writing integrations.What changed
The API shape did not change —
billingModelhas always been on the response. Every affected page now states the criterion per billing model:billingModelcreditssuccess === trueandcreditsRedeemed > 0creditsRedeemedis the amount burned,remainingBalancewhat is leftpay-as-you-gosuccess === trueand a non-emptyorderTx(fiat) /transaction(crypto)"0"— no balance exists on this plan shape...plus the type trap, called out explicitly: the values are strings, so
"0"is truthy whileNumber("0") > 0is false.Pages touched:
skills/nevermined-payments/SKILL.mdand itsautonomous-operations,client-integrationandmcp-paywallreferences.AGENTS.md,.cursor,.windsurf,.clinerules,.github/copilot-instructions.md)..amazonqdoes not list them and was left alone. The Windsurf payments file is 3,938 of its 6,000-character cap after the edit (its router sibling is untouched at 5,984).specs/x402-card-delegation.mdxandspecs/x402-smart-accounts.mdx: receipt-format tables plus a new "Was the buyer charged?" subsection each, and a pay-as-you-goPAYMENT-RESPONSEexample alongside the credits one.products/x402-facilitator/how-it-works.mdx,agents-guide/check-credits.mdx,solutions/api-providers.mdx,integrate/patterns/charge-credits.mdx(a scoping note: those patterns assume a credits plan),development-guide/nevermined-x402.mdx(passbillingModelthrough the receipt the agent builds).integrations/youdotcom.mdxalready carried the correct guidance — no change needed there.One thing beyond the strict brief
api-reference/openapi.jsonis hand-maintained, and itsSettlePermissionResponseschema did not match the endpoint at all: it namedpermissionHash,creditsSettled,remainingCreditsandtxHash— three of which the API does not return — and typed the credit fields as integers rather than strings. This is the published interactive API reference and clients are generated from it, so an integrator looking up "what does settle return" got a wholly fictional answer. It now describes the real response (success,errorReason,payer,transaction,network,billingModel,creditsRedeemed,remainingBalance,orderTx), with the criterion on the schema description. Edited as text, not round-tripped through a JSON serializer.Flagging separately, and not fixed here: the neighbouring x402 schemas look similarly stale (
/x402/permissionsis documented as returningpermissionHash/maxCredits, where the real endpoint returnsaccessToken). That is a bigger, separate cleanup.Drive-by fix, folded in
All six occurrences of
settlement.txHash/settlement.tx_hashin this repo are fixed. That field exists on neither SDK's settle result — both returntransaction— so every one of these examples loggedundefined(or raisedAttributeError) when copied as written.development-guide/nevermined-x402.mdxproducts/x402-facilitator/how-it-works.mdxintegrate/add-to-your-agent/express.mdxonAfterSettlehookskills/nevermined-payments/references/express-integration.mdonAfterSettlehookThe two Express ones are hook callbacks, so the object is whatever the middleware passes rather than a direct settle return — checked, not assumed. Both rails are safe: the x402 branch passes the resolved value of
settlePermissions(aSettlePermissionsResult), and the MPP branch passesMppSettleResult, which extends it.transactionexists on both;txHashappears nowhere in the SDK's x402 or MPP surface.Only the value changes.
transactionHashstays as the integrator's own key name in the receipt they build — renaming that would change what their buyers parse, which is a different call from fixing a field access.The express skill reference is the one that made this worth folding in rather than deferring: it ships to AI coding assistants over the same four routes as the skill itself, so leaving it would keep handing agent builders a field that does not exist.
Verification
mintlify broken-links— no broken links.broken-linksdoes not validate in-page anchors — I mutation-tested it with a deliberately bogus#definitely-not-a-real-anchor-xyzand it still reported success. The one new intra-page anchor (#66-was-the-buyer-charged, into### 6.6 Was the buyer charged?) was therefore verified separately againstgithub-slugger, the slugifier Mintlify uses.api-reference/typescript/**orapi-reference/python/**— those are generated from the SDK repos, and the matching source changes are in feat(x402): surface billingModel so a pay-as-you-go settle can be read as paid payments#432 and feat(x402): surface billing_model so a pay-as-you-go settle can be read as paid payments-py#272.Refs nevermined-io/nvm-monorepo#2999
🤖 Generated with Claude Code
https://claude.ai/code/session_01CVpimo1dBXnTJo9tQSy8SA