feat(billing): x402 USDC payments — top-up, transactions, discovery, pay-per-deploy#3460
feat(billing): x402 USDC payments — top-up, transactions, discovery, pay-per-deploy#3460opencolin wants to merge 16 commits into
Conversation
Adds an x402 (HTTP 402 payment protocol) payment source alongside Stripe: POST /v1/x402/top-up settles a USDC payment via a facilitator and credits the managed wallet through RefillService. Includes x402_transactions table, idempotent settle-before-credit flow with crash recovery, env-gated config, and unit tests. Plan/handoff docs under doc/x402/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Allow X402_NETWORK to be a testnet CAIP-2 id (e.g. eip155:84532 Base Sepolia). Enforce at config-parse time that testnet settlement networks are only permitted when Akash NETWORK is not mainnet, so a testnet payment can never settle and credit a real mainnet balance. Testnet classification lives in a single source (x402-networks.ts). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dpoint Pre-settle guardrails: after facilitator verification and before processSettlement, validate the requirement's network against the configured X402_NETWORK and the payer-authorized asset/amount against the requirement. On mismatch, cancel the verified payment and return an explicit 402 code (WRONG_NETWORK, WRONG_ASSET, AMOUNT_MISMATCH) without ever settling. Discovery: add public GET /v1/x402/discovery (SECURITY_NONE) returning the resource list and accepts. Both the 402 flow and discovery derive from one canonical source (X402Service.getCanonicalRoutes) so advertised accepts cannot drift. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t handling Guarantee a settled x402 payment credits the wallet exactly once and never 500s on a duplicate payment_hash insert. - Credit via an atomic settled -> succeeded transition (markSettledAsSucceeded, conditional UPDATE ... WHERE status = 'settled'); topUpWallet runs only when the transition wins (rowCount 1), so a concurrent retry and the reconcile job credit once. - Catch the DB-level UNIQUE(payment_hash) violation on insert, re-read the winning row, and resume/dedupe (succeeded -> 409, settled -> resume credit, in-flight -> 409) instead of surfacing a 500. - Add reconcileStaleSettled() plus findStaleSettled repo query and the X402_RECONCILE_* config used by the reconcile job. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Recurring pg-boss job that re-drives crediting for x402 transactions stranded in `settled` (captured on-chain but not yet credited) past a configurable threshold, closing the crash-before-credit gap. Idempotent via the exactly-once credit gate. - X402ReconcileSettledHandler (queue x402-reconcile-settled, singleton policy, concurrency 1) mirrors WalletBalanceReloadCheckHandler; runs a pass then self-reschedules in finally so the loop survives a throwing pass. - X402ReconcileJobService seeds/reschedules (cancel-created-then-enqueue with a singletonKey), mirroring WalletReloadJobService; no-op when x402 is disabled. - Registered and seeded in app/providers/jobs.provider.ts alongside the other handlers. Emits a backlog count log each run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add GET /v1/x402/transactions so payers (humans and agents) can read their own top-up history. The query is scoped to the authenticated user id explicitly in the repository (in addition to the CASL ability) so a caller can never read another user's rows. Emit stable machine-readable error codes in x402 error bodies (PAYMENT_REQUIRED / PAYMENT_INVALID / AMOUNT_OUT_OF_BOUNDS / DUPLICATE_PAYMENT / X402_DISABLED), centralised in one module and wired through the controller, service and router. Covered by x402.controller.spec.ts (userId scoping + error codes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a standalone, testnet-default example client (examples/x402) that runs the full 402 -> pay -> credit -> read-back loop, plus USAGE.md (env matrix, curl walkthrough, error codes, treasury custody note, and an explicit no-refund limitation) and v1.0 release notes. The example is not part of the monorepo workspaces, so it never affects the Console build. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nfig Add nullable deployment_dseq and deploy_failed columns to x402_transactions (migration 0033) to record the deployment funded by an x402 payment and flag paid-but-failed deployments. Add repository helpers for linking a deployment, flagging deploy failure, and summing/counting a user's recent transactions for the per-user rate limit and cost ceiling. Add deploy deposit bounds and abuse control config vars. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One x402-paid call settles USDC, credits the Console balance through the same RefillService.topUpWallet money-integrity gate as top-ups, then drives the existing DeploymentWriterService.create with the credited balance - no prior balance required. Extract the shared settle-before-credit path so top-up and deploy funnel through a single credit choke point. If settlement and credit succeed but deployment creation fails, funds stay credited (never reversed on-chain): the row is flagged deployFailed and the endpoint returns 502 DEPLOY_FAILED_FUNDS_CREDITED so the caller knows the money is spendable. Abuse controls (per-user rate limit + cost ceiling) are enforced against recent x402_transactions before settlement. The endpoint requires the same sign UserWallet ability as normal deployment creation, and x402 credits carry no first-purchase/trial bonus. Specs cover success, deploy-failure fallback, rate limit, cost ceiling and duplicate payment with a mocked facilitator and mocked deployment service. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gration # Conflicts: # apps/api/src/billing/controllers/x402/x402.controller.ts # apps/api/src/billing/http-schemas/x402.schema.ts # apps/api/src/billing/repositories/x402-transaction/x402-transaction.repository.ts # apps/api/src/billing/routes/x402/x402.router.ts
…tegration # Conflicts: # apps/api/src/billing/controllers/x402/x402.controller.ts # apps/api/src/billing/http-schemas/x402.schema.ts # apps/api/src/billing/routes/x402/x402.router.ts # apps/api/src/billing/services/x402/x402.service.spec.ts # apps/api/src/billing/services/x402/x402.service.ts
…idate Update the x402 PLAN.md worktree table (v1.0/v1.1/v1.2/v2.0 + integration rows marked done with their branch names) and STATE.md current status to describe feat/x402-integration as the review candidate, listing the now-available endpoints: POST /v1/x402/top-up, GET /v1/x402/transactions, GET /v1/x402/discovery, POST /v1/x402/deploy, plus the x402-reconcile-settled job. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds X402 USDC top-up and pay-per-deploy flows with transaction persistence, exactly-once wallet crediting, reconciliation jobs, public API routes, environment validation, documentation, and a runnable example client. ChangesX402 integration
Estimated code review effort: 5 (Critical) | ~120 minutes Suggested labels: Suggested reviewers: ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install timed out. The project may have too many dependencies for the sandbox. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| @@ -69,6 +69,9 @@ | |||
| "@typescript-eslint/parser": "^7.18.0", | |||
There was a problem hiding this comment.
🔄 Carefully review the package-lock.json diff
Resolve the comment if everything is ok
+ apps/api/node_modules/@x402/hono 2.19.0
+ node_modules/apg-js 4.4.0
+ node_modules/isows 1.0.7
+ node_modules/@signinwithethereum/siwe 4.2.0
+ node_modules/@signinwithethereum/siwe-parser 4.2.0
+ node_modules/tweetnacl 1.0.3
+ node_modules/viem 2.55.2
+ node_modules/viem/node_modules/abitype 1.2.3
+ node_modules/viem/node_modules/eventemitter3 5.0.1
+ node_modules/viem/node_modules/@noble/ciphers 1.3.0
+ node_modules/viem/node_modules/ox 0.14.30
* node_modules/ws 8.18.2 -> 8.21.0
+ node_modules/@x402/core 2.19.0
+ node_modules/@x402/evm 2.19.0
+ node_modules/@x402/extensions 2.19.0
+ node_modules/@x402/extensions/node_modules/ajv 8.20.0
+ node_modules/@x402/extensions/node_modules/jose 5.10.0
+ node_modules/@x402/extensions/node_modules/json-schema-traverse 1.0.0
+ packages/ui/node_modules/@types/react-dom 18.3.7 |
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
apps/api/src/billing/services/x402/x402.service.spec.ts (1)
21-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMove the
TxServiceregistration intosetup().This suite-level
beforeEachbypasses the repository’s explicit per-test setup convention. Initialize the mock alongside the SUT instead.As per coding guidelines and path instructions, unit and service tests must use
setup()instead ofbeforeEach.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/billing/services/x402/x402.service.spec.ts` around lines 21 - 25, Move the TxService mock creation, transaction.mockImplementation setup, and container.registerInstance call from the suite-level beforeEach into the existing setup() helper, alongside the SUT initialization. Remove this beforeEach while preserving the mock behavior for every test.Sources: Coding guidelines, Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/api/src/billing/routes/x402/x402.router.ts`:
- Around line 47-53: Update the 402 response definitions and all affected
branches in the x402 router, including settlement failure and cost-ceiling
handling, so every returned body conforms to its declared schema. Use distinct
schemas or a union for protocol, rejection, and cost-ceiling responses, ensuring
each response includes exactly the fields required by the schema rather than
allowing an empty or undocumented shape.
In `@apps/api/src/billing/services/x402/x402.service.ts`:
- Around line 360-363: The existing payment lookup in the deploy flow accepts a
transaction without verifying ownership. Before calling resolveTerminalOrSettled
or returning it, compare the existing transaction’s userId with input.userId;
for mismatches, return the generic duplicate conflict and do not expose, settle,
credit, or otherwise process the existing row. Keep same-user handling
unchanged.
- Around line 162-163: Update the x402 payment flow around amountUsdCents and
the related amount handling at the additional call sites to reject amounts with
more than two decimal places before creating payment requirements.
Alternatively, normalize the amount once to integer cents and derive both the
payer charge and settlePaymentAndCredit inputs from that same value, preserving
exact consistency.
- Around line 419-427: Update the abuse-limit flow containing countByUserSince
and sumAmountByUserSince to atomically reserve both the request count and spend
in the database before external settlement. Add or reuse a repository operation
that performs the combined check-and-increment under a transaction or locking
mechanism, and release/commit it before calling the settlement API; preserve the
existing rate-limit response when reservation fails and avoid holding any
database transaction open during settlement.
- Around line 366-381: The existing-transaction branch in the settlement flow
can reuse an in-flight pending transaction and invoke processSettlement
concurrently. Update the logic around existing and createTransaction so pending
rows are atomically claimed with a processing lease/state before settlement, or
rejected as duplicates; preserve reuse only for resumable failed attempts and
ensure concurrent requests cannot both proceed.
- Around line 224-253: Separate the error boundary in the deployment flow around
deploymentWriterService.create and x402TransactionRepository.linkDeployment:
catch creation errors only and mark the transaction deployFailed for those
failures. Handle linkDeployment persistence errors in a distinct path that does
not classify the transaction as creation-failed or return a false deployment
failure, while preserving the successful response when linking succeeds and
ensuring awaited errors are handled consistently.
---
Nitpick comments:
In `@apps/api/src/billing/services/x402/x402.service.spec.ts`:
- Around line 21-25: Move the TxService mock creation,
transaction.mockImplementation setup, and container.registerInstance call from
the suite-level beforeEach into the existing setup() helper, alongside the SUT
initialization. Remove this beforeEach while preserving the mock behavior for
every test.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 249dc0c5-787d-4b0b-a65a-47b389f026fd
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (42)
apps/api/drizzle/0032_melodic_onslaught.sqlapps/api/drizzle/0033_x402_deploy_columns.sqlapps/api/drizzle/meta/0032_snapshot.jsonapps/api/drizzle/meta/0033_snapshot.jsonapps/api/drizzle/meta/_journal.jsonapps/api/package.jsonapps/api/src/app/providers/jobs.provider.tsapps/api/src/auth/services/ability/ability.service.tsapps/api/src/billing/config/env.config.spec.tsapps/api/src/billing/config/env.config.tsapps/api/src/billing/config/x402-networks.tsapps/api/src/billing/controllers/x402/x402.controller.spec.tsapps/api/src/billing/controllers/x402/x402.controller.tsapps/api/src/billing/events/x402-reconcile-settled.tsapps/api/src/billing/http-schemas/x402.schema.tsapps/api/src/billing/model-schemas/index.tsapps/api/src/billing/model-schemas/x402-transaction/x402-transaction.schema.tsapps/api/src/billing/repositories/index.tsapps/api/src/billing/repositories/x402-transaction/x402-transaction.repository.tsapps/api/src/billing/routes/index.tsapps/api/src/billing/routes/x402/x402.router.tsapps/api/src/billing/services/x402-reconcile-job/x402-reconcile-job.service.spec.tsapps/api/src/billing/services/x402-reconcile-job/x402-reconcile-job.service.tsapps/api/src/billing/services/x402-reconcile-settled/x402-reconcile-settled.handler.spec.tsapps/api/src/billing/services/x402-reconcile-settled/x402-reconcile-settled.handler.tsapps/api/src/billing/services/x402/x402-error-codes.tsapps/api/src/billing/services/x402/x402-http-server-factory.service.tsapps/api/src/billing/services/x402/x402.service.spec.tsapps/api/src/billing/services/x402/x402.service.tsapps/api/src/routers/open-api-handlers.tsdoc/x402/PLAN.mddoc/x402/RELEASE-NOTES-v1.0.mddoc/x402/RELEASE-NOTES-v1.1.mddoc/x402/RELEASE-NOTES-v1.2.mddoc/x402/RELEASE-NOTES-v2.0.mddoc/x402/STATE.mddoc/x402/USAGE.mdexamples/x402/.env.exampleexamples/x402/README.mdexamples/x402/package.jsonexamples/x402/top-up.tsexamples/x402/tsconfig.json
| 402: { | ||
| description: "Payment required or payment invalid/failed to settle", | ||
| content: { | ||
| "application/json": { | ||
| schema: X402PaymentRequiredResponseSchema | ||
| } | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Make every 402 response match its declared schema.
X402PaymentRequiredResponseSchema requires x402Version and accepts, but the settlement-failure test permits an empty body, while cost-ceiling-exceeded returns neither field. Generated clients therefore receive undocumented response shapes.
Use distinct schemas/a union for protocol, rejection, and cost-ceiling responses, or normalize every branch.
Also applies to: 73-82, 131-136, 194-206
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/api/src/billing/routes/x402/x402.router.ts` around lines 47 - 53, Update
the 402 response definitions and all affected branches in the x402 router,
including settlement failure and cost-ceiling handling, so every returned body
conforms to its declared schema. Use distinct schemas or a union for protocol,
rejection, and cost-ceiling responses, ensuring each response includes exactly
the fields required by the schema rather than allowing an empty or undocumented
shape.
| const amountUsdCents = Math.round(amountUsd * 100); | ||
| const settlement = await this.settlePaymentAndCredit(context, { userId, amountUsdCents }, verified); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Reject fractional-cent amounts before generating payment requirements.
The payer is charged from the original decimal input, while Console credit is calculated with Math.round(amount * 100). Inputs such as 1.005 therefore charge and credit different values. Require at most two decimal places or normalize once to integer cents and derive both values from that integer.
Also applies to: 207-217, 604-605
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/api/src/billing/services/x402/x402.service.ts` around lines 162 - 163,
Update the x402 payment flow around amountUsdCents and the related amount
handling at the additional call sites to reject amounts with more than two
decimal places before creating payment requirements. Alternatively, normalize
the amount once to integer cents and derive both the payer charge and
settlePaymentAndCredit inputs from that same value, preserving exact
consistency.
| try { | ||
| const deployment = await this.deploymentWriterService.create({ sdl: input.sdl, deposit: input.deposit, userId }); | ||
| await this.x402TransactionRepository.linkDeployment(transaction.id, deployment.dseq); | ||
|
|
||
| this.logger.info({ | ||
| event: "X402_DEPLOY_SUCCEEDED", | ||
| transactionId: transaction.id, | ||
| userId, | ||
| amountUsdCents: transaction.amount, | ||
| deploymentDseq: deployment.dseq, | ||
| settlementTxHash: settlement.settlementTxHash | ||
| }); | ||
|
|
||
| return { | ||
| type: "success", | ||
| headers: settlement.headers, | ||
| data: { | ||
| transactionId: transaction.id, | ||
| amountUsdCents: transaction.amount, | ||
| network: transaction.network, | ||
| settlementTxHash: settlement.settlementTxHash, | ||
| payerAddress: settlement.payerAddress, | ||
| deploymentDseq: deployment.dseq, | ||
| manifest: deployment.manifest, | ||
| signTx: deployment.signTx | ||
| } | ||
| }; | ||
| } catch (error) { | ||
| const message = error instanceof Error ? error.message : "Deployment creation failed"; | ||
| await this.x402TransactionRepository.markDeployFailed(transaction.id, message); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Do not classify deployment-link persistence failures as creation failures.
The try block includes both deploymentWriterService.create() and linkDeployment(). If deployment creation succeeds but linking fails, the catch marks the transaction deployFailed and tells the client creation failed even though a deployment now exists.
Limit this catch to creation, then handle link persistence separately without returning a false deployment failure.
As per path instructions, verify async error handling and cross-layer consistency.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/api/src/billing/services/x402/x402.service.ts` around lines 224 - 253,
Separate the error boundary in the deployment flow around
deploymentWriterService.create and x402TransactionRepository.linkDeployment:
catch creation errors only and mark the transaction deployFailed for those
failures. Handle linkDeployment persistence errors in a distinct path that does
not classify the transaction as creation-failed or return a false deployment
failure, while preserving the successful response when linking succeeds and
ensuring awaited errors are handled consistently.
Source: Path instructions
| const existing = await this.x402TransactionRepository.findByPaymentHash(paymentHash); | ||
| const resolvedExisting = await this.resolveTerminalOrSettled(existing, cancellationDispatcher); | ||
| if (resolvedExisting) { | ||
| return resolvedExisting; |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win
Bind existing payments to the authenticated user.
findByPaymentHash() is global, but the returned transaction is accepted without checking existing.userId === input.userId. A cross-account replay can expose another user's transaction ID; if it is settled, the deploy flow credits the original owner and then continues deployment creation for the replaying user.
Return a generic duplicate conflict for mismatched owners and never return or process the existing row.
As per path instructions, verify user-scoped authorization and auth-bypass risks in API services.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/api/src/billing/services/x402/x402.service.ts` around lines 360 - 363,
The existing payment lookup in the deploy flow accepts a transaction without
verifying ownership. Before calling resolveTerminalOrSettled or returning it,
compare the existing transaction’s userId with input.userId; for mismatches,
return the generic duplicate conflict and do not expose, settle, credit, or
otherwise process the existing row. Keep same-user handling unchanged.
Source: Path instructions
| // `existing` is now undefined or a resumable pending/failed attempt this request owns. | ||
| let transaction: X402TransactionOutput; | ||
| if (existing) { | ||
| transaction = existing; | ||
| } else { | ||
| const created = await this.createTransaction( | ||
| { userId: input.userId, amountUsdCents: input.amountUsdCents, paymentRequirements, paymentHash }, | ||
| cancellationDispatcher | ||
| ); | ||
| if (created.type === "resolved") { | ||
| return created.result; | ||
| } | ||
| transaction = created.transaction; | ||
| } | ||
|
|
||
| const settleResult = await httpServer.processSettlement(paymentPayload, paymentRequirements, declaredExtensions, { request: context }); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift
Do not let a second request reuse an in-flight pending transaction.
A request arriving after the first insert but before settlement reads the existing pending row and also calls processSettlement(). The insert-race handling does not cover this timing, so the same signed payment may be settled concurrently.
Atomically claim the row with a processing lease/state, or reject existing pending rows as duplicates.
As per path instructions, check concurrency hazards in get → check → mutate flows.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/api/src/billing/services/x402/x402.service.ts` around lines 366 - 381,
The existing-transaction branch in the settlement flow can reuse an in-flight
pending transaction and invoke processSettlement concurrently. Update the logic
around existing and createTransaction so pending rows are atomically claimed
with a processing lease/state before settlement, or rejected as duplicates;
preserve reuse only for resumable failed attempts and ensure concurrent requests
cannot both proceed.
Source: Path instructions
| const recentCount = await this.x402TransactionRepository.countByUserSince(userId, since); | ||
| if (recentCount >= this.config.X402_ABUSE_MAX_REQUESTS) { | ||
| this.logger.warn({ event: "X402_RATE_LIMITED", userId, recentCount, max: this.config.X402_ABUSE_MAX_REQUESTS, windowSeconds }); | ||
| return { type: "rate-limited", retryAfterSeconds: windowSeconds }; | ||
| } | ||
|
|
||
| const recentSpendCents = await this.x402TransactionRepository.sumAmountByUserSince(userId, since); | ||
| const ceilingCents = Math.round(this.config.X402_ABUSE_MAX_SPEND_USD * 100); | ||
| if (recentSpendCents + amountUsdCents > ceilingCents) { |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
Make abuse-limit reservation atomic.
Concurrent requests can all read the same count/spend, pass both checks, and then settle, exceeding both configured limits. Reserve the request count and amount atomically in the database before settlement; do not hold the transaction open during the external settlement call.
As per path instructions, check race conditions in get → check → mutate patterns and avoid external API calls inside database transactions.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/api/src/billing/services/x402/x402.service.ts` around lines 419 - 427,
Update the abuse-limit flow containing countByUserSince and sumAmountByUserSince
to atomically reserve both the request count and spend in the database before
external settlement. Add or reuse a repository operation that performs the
combined check-and-increment under a transaction or locking mechanism, and
release/commit it before calling the settlement API; preserve the existing
rate-limit response when reservation fails and avoid holding any database
transaction open during settlement.
Source: Path instructions
What
Adds an x402 (HTTP 402 payment protocol) USDC payment layer to the Console API, alongside Stripe. AI agents and crypto-native users holding only a wallet +
x-api-keycan fund Console credits — and even create+fund a deployment in a single paid call — with no credit card. This removes the "Payment method: credit card only" limitation documented for the Managed Wallet API.New surface (all env-gated behind
X402_ENABLED, off by default)POST /v1/x402/top-up?amount=<usd>X-PAYMENT→ 402 with payment requirements; signed retry → facilitator verify → settle → credit via the existingRefillService.topUpWalletchoke pointGET /v1/x402/transactionsGET /v1/x402/discoveryPOST /v1/x402/deployPlus:
x402-reconcile-settledpg-boss job (re-drives stranded settled-but-uncredited payments),x402_transactionstable + migrations,examples/x402/standalone agent client (Base Sepolia defaults), and operator/user docs underdoc/x402/.Design notes
status='settled' → 'succeeded'transition, payments are idempotent by a DB-unique payment hash, and a reconciliation job guarantees no settled USDC is ever stranded. Funding flows exclusively throughRefillService.topUpWallet— no bespoke credit path anywhere, including pay-per-deploy.DEPLOY_FAILED_FUNDS_CREDITED) — never an on-chain reversal, never lost funds.eip155:84532) accepts map to the sandbox network only; config validation makes it impossible for a testnet settlement to credit a mainnet balance.WRONG_NETWORK,WRONG_ASSET,AMOUNT_MISMATCH,AMOUNT_OUT_OF_BOUNDS,DUPLICATE_PAYMENT, …) before any settlement is attempted. Abuse controls (per-user rate limit + cost ceiling) ship with the paid deploy path.@x402/core/@x402/evm/@x402/honov2.19.0; facilitator URL configurable (X402_FACILITATOR_URL, defaulthttps://x402.org/facilitator).Config
X402_ENABLED,X402_PAY_TO_ADDRESS(treasury EVM address),X402_NETWORK(CAIP-2, defaulteip155:8453),X402_FACILITATOR_URL,X402_MIN_TOP_UP_USD/X402_MAX_TOP_UP_USD, plus reconcile-job and abuse-control knobs. Seedoc/x402/USAGE.mdfor the full matrix, a curl walkthrough of the 402→pay→credit loop, treasury custody notes, and the explicit no-refund statement.Testing
vitest --project=unit), covering: exactly-once credit under concurrent retry + reconcile race, crash-before-credit recovery, duplicate payment 409, settlement failure, testnet/mainnet firewall, pre-settle guardrails, discovery/402 consistency, pay-per-deploy success + credited-fallback, rate limit/cost ceiling, and IDOR scoping on the transactions listing.tsc -p tsconfig.build.json --noEmitandeslint --quietclean.examples/x402/README.md).Notes for reviewers
doc/x402/PLAN.mdanddoc/x402/council-decision.json.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Documentation