← Back to PRs

#14290: feat: x402 payment info parsing and configurable billing recovery [AI]

by stubbi open 2026-02-11 22:09 View on GitHub →
agents stale size: L
## Summary Billing/402 errors currently disable auth profiles for **5–24 hours** with exponential backoff. This is correct for traditional API keys where top-up requires manual action, but too aggressive for prepaid credit systems where an agent can top up in minutes via x402-compatible endpoints. This PR adds three capabilities: - **x402 payment info extraction** — new `parseX402PaymentInfo()` function parses structured payment data (top-up URL, balance, payment scheme) from billing error response bodies that follow the [x402 v2 protocol](https://x402.org) - **Rich billing error messages** — `formatBillingErrorMessage()` replaces the generic "check your dashboard" message with actionable info: remaining balance and a direct top-up URL when available - **Configurable billing recovery mode** — new config option `auth.cooldowns.billingRecoveryMode` with three modes: - `"disable"` (default): existing behavior, 5–24h exponential backoff - `"retry"`: 5 minute cooldown, for systems with fast automated top-up - `"notify"`: no cooldown at all, profile stays available immediately The x402 payment info is also propagated through `FailoverError.paymentInfo` so higher-level consumers (agent frameworks, skills, UIs) can programmatically access the top-up URL and balance without parsing error message text. ## Motivation Agents using prepaid credit gateways (e.g., OpenClaw Fuel via Bifrost) hit a dead end when credits run out: the profile gets disabled for hours, all fallback models through the same gateway fail, and the user sees a generic "check your dashboard" message. With x402 support: 1. The agent sees *exactly* what happened: `$0.00 of $20.00 remaining` 2. The agent knows *where* to top up: `https://openclaw.rocks/fuel` 3. With `billingRecoveryMode: "retry"`, the profile is available again after 5 minutes instead of 5 hours 4. With `billingRecoveryMode: "notify"`, the profile stays available immediately for retry after top-up ## Changes | File | Change | |---|---| | `src/agents/pi-embedded-helpers/types.ts` | Add `X402PaymentInfo` type | | `src/agents/pi-embedded-helpers/errors.ts` | Add `parseX402PaymentInfo()` and `formatBillingErrorMessage()`; use them in `formatAssistantErrorText()` and `sanitizeUserFacingText()` | | `src/agents/pi-embedded-helpers.ts` | Export new symbols | | `src/agents/failover-error.ts` | Add `paymentInfo` field to `FailoverError`; extract x402 info in `coerceToFailoverError()` | | `src/agents/pi-embedded-runner/run.ts` | Pass x402 payment info through billing `FailoverError` throws | | `src/config/types.auth.ts` | Add `billingRecoveryMode` to `AuthConfig.cooldowns` | | `src/config/zod-schema.ts` | Add `billingRecoveryMode` enum validation | | `src/config/schema.hints.ts` | Add hint for `billingRecoveryMode` | | `src/config/schema.field-metadata.ts` | Add label and description for `billingRecoveryMode` | | `src/agents/auth-profiles/usage.ts` | Implement `"retry"` (5min cooldown) and `"notify"` (no cooldown) modes; clear stale state on mode switch | ## Config example ```yaml auth: cooldowns: billingRecoveryMode: retry # 5 min cooldown instead of 5 hours billingBackoffHoursByProvider: bifrost: 0.08 # ~5 min (still works as before for per-provider override) ``` ## Test plan - [x] `parseX402PaymentInfo` — 8 test cases covering JSON extraction, HTTP prefix, x402 `accepts` array, edge cases - [x] `formatBillingErrorMessage` — 4 test cases covering generic fallback, balance + URL, partial info - [x] `FailoverError.paymentInfo` — 3 test cases covering x402 extraction, non-billing exclusion, plain billing errors - [x] `billingRecoveryMode` — 5 test cases: `"disable"`, `"retry"`, `"notify"` modes + mode-switch regression tests (disable→notify, disable→retry) - [x] Full suite: `pnpm build && pnpm check && pnpm test` — 267 tests pass, 0 failures - [x] TypeScript compiles with zero errors - [x] Formatting + lint (oxlint) pass with zero errors/warnings ## Backwards compatibility - Default `billingRecoveryMode` is `"disable"` — existing behavior is unchanged - `X402PaymentInfo` is additive (new optional field on `FailoverError`) - Error messages are enriched only when x402 info is present; generic message unchanged otherwise - All existing tests pass without modification --- ## AI Disclosure - [x] **AI-assisted**: This PR was written with Claude Code (Claude Opus 4.6) - [x] **Degree of testing**: Fully tested — 20 new tests across 3 test files, plus full `pnpm build && pnpm check && pnpm test` pipeline (267 tests, 0 failures) - [x] **I understand what this code does**: `parseX402PaymentInfo` extracts structured payment metadata from JSON error bodies; `formatBillingErrorMessage` renders actionable user-facing messages from that metadata; `billingRecoveryMode` gates the cooldown logic in `computeNextProfileUsageStats` between three strategies (exponential disable, short cooldown, or no cooldown). The x402 info propagates through `FailoverError.paymentInfo` so consumers don't need to re-parse error text.

Most Similar PRs