Skip to content

docs(x402): state the settle success criterion per billing model - #369

Open
r-marques wants to merge 5 commits into
mainfrom
docs/2999-payg-settle-success-criterion
Open

docs(x402): state the settle success criterion per billing model#369
r-marques wants to merge 5 commits into
mainfrom
docs/2999-payg-settle-success-criterion

Conversation

@r-marques

@r-marques r-marques commented Sep 8, 2026

Copy link
Copy Markdown
Member

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) or transaction (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.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. 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 — billingModel has always been on the response. Every affected page now states the criterion per billing model:

billingModel Success criterion Credit fields
credits success === true and creditsRedeemed > 0 creditsRedeemed is the amount burned, remainingBalance what is left
pay-as-you-go success === true and a non-empty orderTx (fiat) / transaction (crypto) always the string "0" — no balance exists on this plan shape

...plus the type trap, called out explicitly: the values are strings, so "0" is truthy while Number("0") > 0 is false.

Pages touched:

  • Skillskills/nevermined-payments/SKILL.md and its autonomous-operations, client-integration and mcp-paywall references.
  • IDE rule files — the five that list the settle response fields (AGENTS.md, .cursor, .windsurf, .clinerules, .github/copilot-instructions.md). .amazonq does 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).
  • Specsspecs/x402-card-delegation.mdx and specs/x402-smart-accounts.mdx: receipt-format tables plus a new "Was the buyer charged?" subsection each, and a pay-as-you-go PAYMENT-RESPONSE example alongside the credits one.
  • Guidesproducts/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 (pass billingModel through the receipt the agent builds).

integrations/youdotcom.mdx already carried the correct guidance — no change needed there.

One thing beyond the strict brief

api-reference/openapi.json is hand-maintained, and its SettlePermissionResponse schema did not match the endpoint at all: it named permissionHash, creditsSettled, remainingCredits and txHash — 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/permissions is documented as returning permissionHash / maxCredits, where the real endpoint returns accessToken). That is a bigger, separate cleanup.

Drive-by fix, folded in

All six occurrences of settlement.txHash / settlement.tx_hash in this repo are fixed. That field exists on neither SDK's settle result — both return transaction — so every one of these examples logged undefined (or raised AttributeError) when copied as written.

File What it was
development-guide/nevermined-x402.mdx receipt construction, TS + Python
products/x402-facilitator/how-it-works.mdx receipt construction, TS + Python
integrate/add-to-your-agent/express.mdx onAfterSettle hook
skills/nevermined-payments/references/express-integration.md onAfterSettle hook

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

Only the value changes. transactionHash stays 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

Refs nevermined-io/nvm-monorepo#2999

🤖 Generated with Claude Code

https://claude.ai/code/session_01CVpimo1dBXnTJo9tQSy8SA

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
@mintlify

mintlify Bot commented Sep 8, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
Nevermined 🟢 Ready View Preview Sep 8, 2026, 1:33 PM

💡 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
r-marques marked this pull request as ready for review September 8, 2026 14:11
@r-marques
r-marques requested a review from a team as a code owner September 8, 2026 14:11
@aaitor

aaitor commented Sep 8, 2026

Copy link
Copy Markdown
Member

No description provided.

r-marques and others added 2 commits September 8, 2026 18:12
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants