[HOLD] x402 USDC credits top-up - #112
Conversation
…not merge] POST /v1/billing/topup-x402/:tier ($5/$20/$50): an API-key-authenticated agent pays the 402 in USDC through the EXISTING x402 middleware and gets credits at a pack-equivalent rate (CREDIT_PACKS is the rate authority; never cheaper than the best pack, never worse than the starter pack's $0.003/credit). - middleware/x402.ts: optional X402MiddlewareOptions (price override, resourceUrl override, requirePayment) + req.x402Settlement facts after a success===true settle. Tool-route behavior unchanged (regression-tested). - lib/x402Topup.ts: pure tier math (integer, ceil in buyer's favor) + injectable idempotent grant core (dedupe on settlement tx/nonce via Purchase.stripeId unique - same discipline as the Stripe webhook; grant + payment record in ONE transaction). - routes/topupX402.ts: requireAuth -> per-tier x402 gate -> grant. Fail-closed when the gate is inactive; webhook/email/admin-alert parity with card purchases; loud alert if a settle ever lands uncredited. - X402_PRICES and all advertised price surfaces untouched; price drift guard passes unchanged. - tests: 19 fixtures in api/tests/x402-topup.test.mjs (tier math pinned, challenge overrides, requirePayment, grant idempotency/races). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issue.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit b1d8a10. Configure here.
| if (!txHash && !nonce) { | ||
| sendAdminAlert("⚠️ x402 top-up settled without a dedupe key", `Settle succeeded but returned no transaction hash and the payment carried no nonce.\nagent=${agent.id} tier=$${tier.amountCents / 100} key=${settlementKey}`).catch(() => {}); | ||
| } | ||
| const dedupeId = `x402:${settlementKey}`; |
There was a problem hiding this comment.
Random dedupe key allows double credit
Medium Severity
When settlement returns no transaction hash and extractNonce finds no nonce, each top-up request builds a new unkeyed: dedupe id via randomUUID(). Repeat successful settles on the same payment (or retries that pass the gate again) can insert distinct Purchase.stripeId rows and grant credits more than once, contrary to settlement-based idempotency.
Reviewed by Cursor Bugbot for commit b1d8a10. Configure here.


⛔ DO NOT MERGE — build-only PR (GROWTH_50 item #8), held for conductor review
USDC → credits top-up: the crypto crowd is the only segment that has ever paid, and until now they had no card-free path to credits. This gives them one, on the exact x402 rails that already move money in production.
What it does
POST /v1/billing/topup-x402/:tier— tiers5/20/50($5, $20, $50):Authorization: Bearer <api_key>and no payment header → 402 challenge priced at the tier (newrequirePaymentmiddleware option: on this route an API key does not bypass the gate — the payment IS the point; auth only establishes whose credits).PAYMENT-SIGNATURE(or legacyX-PAYMENT) + the same API key.requireAuthidentifies the account, then the existing x402 middleware verifies + settles via the CDP facilitator — zero hand-rolled payment verification.settle success===truethe handler grants credits atomically with aPurchaserecord, idempotent on the settlement id (Purchase.stripeId = "x402:<tx-or-nonce>", unique) — the same dedupe discipline as the Stripe webhook. Replays/races can never double-credit (unit-tested).GET /v1/billing/topup-x402— public tier catalog + how-to.Rates — CREDIT_PACKS is the authority (never our own numbers)
Derived at startup from
routes/billing.tsCREDIT_PACKS(now exported), integer math, ceil'd ≤1 credit in the buyer's favor:Tier credits are pinned in tests so a pack price change fails loud (mirrors the intent-funnel pack pin).
Money-safety properties
req.x402Paid === trueset by THIS request's settle (wallet-unconfigured/Stripe-only mode → 503, never a free grant).ALLOWED_ONETIME_CREDITSparity).What deliberately did NOT change
X402_PRICESand every advertised price surface (openapi.json, tools.json, llms*.txt, discovery.ts, mcp/server.ts) — tier prices ride in as middleware options, soscripts/check-price-drift.mjsneeds no changes and passes unchanged (verified:OK — 89 charged tools, 88 x402 prices, 3 packs + 4 subscriptions)./webhooksalias — the new router mounts on/v1/billingonly.Files
api/src/middleware/x402.ts— optionalX402MiddlewareOptions(price,resourceUrl,requirePayment) +req.x402Settlementafter successful settle + real agent attribution in the ApiRequest log when auth ran firstapi/src/lib/x402Topup.ts— new: pure tier math + injectable idempotent grant coreapi/src/routes/topupX402.ts— new: catalog +requireAuth → x402 gate → grantapi/src/routes/billing.ts—export const CREDIT_PACKS(+2 comment lines, nothing else)api/src/index.ts— mountapi/tests/x402-topup.test.mjs— new: 19 fixtures (tier math, challenge overrides, requirePayment, grant idempotency/races) — modeled on the PR fix(x402): pass V1 payment payloads through to CDP unchanged (payment_invalid on all real x402 payments) #62 fixture patternapi/package.json— test added to the npm test chainVerification (all run locally in the worktree)
npx tsc --noEmit→ cleannode tests/x402-topup.test.mjs→ 19/19 passx402-v1-passthrough/x402-sell-copy/x402-v2/intent-funnel/critical-regressions/signup-firstcall→ all passnode scripts/check-price-drift.mjs→ OK, unchangednode api/scripts/security-gate.cjs→ passedHeld-back items for the conductor (before/at merge)
🤖 Generated with Claude Code
Note
High Risk
New money path that mutates balances and Purchase rows; mitigations (fail-closed grant, allow-list, unique dedupe, alerts) are strong but this is still payment/credit infrastructure worth careful review and a live settle test before merge.
Overview
Adds a USDC → credits path for authenticated agents:
GET/POST /v1/billing/topup-x402/:tier($5 / $20 / $50) mounted on/v1/billingonly (not the/webhooksbilling alias).Flow is auth first (
requireAuth), then existing x402 middleware with new options:requirePayment(API keys no longer bypass the 402),price, andresourceUrlso tier amounts never enterX402_PRICES. Aftersettle success===true, credits grant in one Prisma transaction with aPurchaserow; dedupe usesPurchase.stripeId = "x402:<tx-or-nonce>"(same pattern as Stripe).req.x402Settlementis set on successful settle for the grant handler; ApiRequest logging can attribute toreq.agentwhen auth ran before the gate.lib/x402Topup.tsholds pack-equivalent tier math from exportedCREDIT_PACKSand injectablegrantTopupCredits.tests/x402-topup.test.mjspins tier credits and idempotency;package.jsontest chain includes it.Default tool x402 behavior stays opt-in unchanged when middleware options are omitted.
Reviewed by Cursor Bugbot for commit b1d8a10. Configure here.