error_detail on a 402 $refs the shared Error schema, which has no payment_rejected member. So the field is invisible to codegen: a generated client has no typed way to reach it, and anyone reading the spec cannot discover that a 402 distinguishes "you have not paid" from "you paid and it was REJECTED, here is the field to fix."
That distinction is the whole point of the gateway change — without it in the spec, autonomous/agent callers (the designed consumer of this rail) still cannot self-correct from a generated SDK.
Grounded
openapi.yaml @ main: grep -n payment_rejected → no match.
X402PaymentRequired (line 3527) sets error_detail: $ref: '#/components/schemas/Error'.
- The gateway now emits, on a rejected payment only:
{
"x402Version": 1,
"error": "payment required",
"error_detail": {
"code": "PAYMENT_REQUIRED",
"message": "The submitted payment was REJECTED (see error_detail.payment_rejected). ...",
"payment_rejected": { "reason": "invalid_permit_header", "rail": "tempo-pathusd" },
"suggestions": ["..."]
},
"accepts": [ ... ],
"next_action": { ... }
}
payment_rejected is conditional and additive — the key is absent entirely when the caller simply has not paid, so the existing no-payment body is unchanged.
Shape to add
payment_rejected (object, optional), with:
reason (string) — a closed, server-controlled vocabulary. Never free text, never upstream/on-chain error text. Beyond the specific enum members there are four stable fallback markers a client must handle: payment_rejected (generic), settle_rejected, broadcast_rejected, quote_token_rejected (naming the settlement stage that failed).
rail (string) — the settlement rail: one of base-usdc, tempo-pathusd, wave-hub, unknown.
Clients should treat reason as an open enum (match known members, degrade gracefully on an unknown one) since members are added as rails are added.
The same error_detail.payment_rejected shape also appears on the streaming STREAM_HALTED 402 body, deliberately at the same read path so a client needs one lookup for both.
Notes
error_detailon a 402$refs the sharedErrorschema, which has nopayment_rejectedmember. So the field is invisible to codegen: a generated client has no typed way to reach it, and anyone reading the spec cannot discover that a 402 distinguishes "you have not paid" from "you paid and it was REJECTED, here is the field to fix."That distinction is the whole point of the gateway change — without it in the spec, autonomous/agent callers (the designed consumer of this rail) still cannot self-correct from a generated SDK.
Grounded
openapi.yaml@main:grep -n payment_rejected→ no match.X402PaymentRequired(line 3527) setserror_detail: $ref: '#/components/schemas/Error'.{ "x402Version": 1, "error": "payment required", "error_detail": { "code": "PAYMENT_REQUIRED", "message": "The submitted payment was REJECTED (see error_detail.payment_rejected). ...", "payment_rejected": { "reason": "invalid_permit_header", "rail": "tempo-pathusd" }, "suggestions": ["..."] }, "accepts": [ ... ], "next_action": { ... } }payment_rejectedis conditional and additive — the key is absent entirely when the caller simply has not paid, so the existing no-payment body is unchanged.Shape to add
payment_rejected(object, optional), with:reason(string) — a closed, server-controlled vocabulary. Never free text, never upstream/on-chain error text. Beyond the specific enum members there are four stable fallback markers a client must handle:payment_rejected(generic),settle_rejected,broadcast_rejected,quote_token_rejected(naming the settlement stage that failed).rail(string) — the settlement rail: one ofbase-usdc,tempo-pathusd,wave-hub,unknown.Clients should treat
reasonas an open enum (match known members, degrade gracefully on an unknown one) since members are added as rails are added.The same
error_detail.payment_rejectedshape also appears on the streamingSTREAM_HALTED402 body, deliberately at the same read path so a client needs one lookup for both.Notes
$refthe wrong body for 402) and PR feat(openapi): spec the MoQ join-token mint surface #30 (which introducedX402PaymentRequired). This is distinct: 402 responses model the wrong body — /render, /av/remux, /av/demux all $ref Error, which prod does not return #31 is about endpoints pointing at the wrong schema; this is about a field missing from the schema itself. Both should probably land together when the 402 contract is next touched.