Skip to content

402 contract omits error_detail.payment_rejected — generated clients cannot discover the deny reason #46

Description

@yakimoto

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_rejectedno 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions