Skip to content

feat(billing): x402 USDC payments — top-up, transactions, discovery, pay-per-deploy#3460

Open
opencolin wants to merge 16 commits into
akash-network:mainfrom
opencolin:feat/x402-integration
Open

feat(billing): x402 USDC payments — top-up, transactions, discovery, pay-per-deploy#3460
opencolin wants to merge 16 commits into
akash-network:mainfrom
opencolin:feat/x402-integration

Conversation

@opencolin

@opencolin opencolin commented Jul 18, 2026

Copy link
Copy Markdown

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-key can 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)

Endpoint Purpose
POST /v1/x402/top-up?amount=<usd> x402-gated top-up: no X-PAYMENT → 402 with payment requirements; signed retry → facilitator verify → settle → credit via the existing RefillService.topUpWallet choke point
GET /v1/x402/transactions Caller-scoped, paginated payment history
GET /v1/x402/discovery Public discovery of x402 resources, derived from the same accepts source as the 402 bodies (single source of truth)
POST /v1/x402/deploy Pay-per-deploy: one x402-paid call creates and funds a deployment with zero prior balance

Plus: x402-reconcile-settled pg-boss job (re-drives stranded settled-but-uncredited payments), x402_transactions table + migrations, examples/x402/ standalone agent client (Base Sepolia defaults), and operator/user docs under doc/x402/.

Design notes

  • Money integrity first. Settlement happens before crediting (crediting is irreversible), crediting is exactly-once via a conditional 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 through RefillService.topUpWallet — no bespoke credit path anywhere, including pay-per-deploy.
  • Pay-per-deploy fail-safe. If settlement succeeds but deployment creation fails, funds remain credited to the caller's Console balance and the transaction row is flagged (DEPLOY_FAILED_FUNDS_CREDITED) — never an on-chain reversal, never lost funds.
  • Testnet firewall. Base Sepolia (eip155:84532) accepts map to the sandbox network only; config validation makes it impossible for a testnet settlement to credit a mainnet balance.
  • Guardrails before settle. Wrong network / wrong asset / amount mismatch are rejected with stable machine-readable codes (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.
  • SDK: @x402/core / @x402/evm / @x402/hono v2.19.0; facilitator URL configurable (X402_FACILITATOR_URL, default https://x402.org/facilitator).

Config

X402_ENABLED, X402_PAY_TO_ADDRESS (treasury EVM address), X402_NETWORK (CAIP-2, default eip155:8453), X402_FACILITATOR_URL, X402_MIN_TOP_UP_USD / X402_MAX_TOP_UP_USD, plus reconcile-job and abuse-control knobs. See doc/x402/USAGE.md for the full matrix, a curl walkthrough of the 402→pay→credit loop, treasury custody notes, and the explicit no-refund statement.

Testing

  • 42 unit/functional-style specs green (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 --noEmit and eslint --quiet clean.
  • Not yet exercised: a live end-to-end settlement against a real facilitator with a funded wallet (manual steps documented in examples/x402/README.md).

Notes for reviewers

  • Development history: this branch integrates four internally-reviewed increments (v1.0 top-up/read-back, v1.1 money-integrity, v1.2 sandbox+discovery, v2.0 pay-per-deploy); the roadmap and decision log live in doc/x402/PLAN.md and doc/x402/council-decision.json.
  • We're happy to split this into smaller PRs (e.g. top-up core first) if that's easier to review.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added USDC top-ups through x402 payment flows.
    • Added pay-per-deploy payments with managed deployment creation.
    • Added transaction history and public payment discovery endpoints.
    • Added stable payment error codes and user-scoped transaction access.
    • Added automatic reconciliation for settled payments that have not yet been credited.
    • Added a runnable x402 top-up example with testnet safety defaults.
  • Bug Fixes

    • Prevented duplicate wallet credits during concurrent payment processing.
    • Added safeguards against network, asset, and amount mismatches before settlement.
    • Improved handling of deployment failures after payment settlement.
  • Documentation

    • Added setup, usage, release, and operational guidance for x402 payments.

opencolin and others added 15 commits July 17, 2026 14:41
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>
@opencolin
opencolin requested a review from a team as a code owner July 18, 2026 00:52
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds 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.

Changes

X402 integration

Layer / File(s) Summary
Contracts, configuration, and persistence
apps/api/drizzle/*, apps/api/src/billing/config/*, apps/api/src/billing/http-schemas/*, apps/api/src/billing/model-schemas/*
Adds X402 transaction schema and migrations, environment controls, network safety validation, API schemas, stable error codes, authorization, and X402 dependencies.
Payment lifecycle and transaction operations
apps/api/src/billing/services/x402/*, apps/api/src/billing/repositories/x402-transaction/*
Implements payment verification, pre-settlement validation, settlement, idempotent crediting, deployment handling, abuse limits, discovery generation, and transaction queries.
HTTP surface
apps/api/src/billing/controllers/x402/*, apps/api/src/billing/routes/x402/*, apps/api/src/routers/open-api-handlers.ts
Adds authenticated top-up, deploy, and transaction-list endpoints plus public discovery, with stable response codes and route registration.
Reconciliation loop
apps/api/src/billing/events/*, apps/api/src/billing/services/x402-reconcile-*, apps/api/src/app/providers/jobs.provider.ts
Adds stale-settlement reconciliation with singleton execution, self-rescheduling, startup registration, and tests.
Documentation and example client
doc/x402/*, examples/x402/*
Documents configuration, API behavior, release notes, integration state, and a testnet-first signed top-up example with transaction polling.

Estimated code review effort: 5 (Critical) | ~120 minutes

Suggested labels: experienced-contributor

Suggested reviewers: stalniy

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread package-lock.json
@@ -69,6 +69,9 @@
"@typescript-eslint/parser": "^7.18.0",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔄 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  

@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​x402/​hono@​2.19.07510010096100
Added@​x402/​core@​2.19.07910010096100
Added@​x402/​evm@​2.19.08010010096100
Addedviem@​2.55.29710010098100

View full report

@socket-security

Copy link
Copy Markdown

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.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm apg-js is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: package-lock.jsonnpm/@x402/hono@2.19.0npm/apg-js@4.4.0

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/apg-js@4.4.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Move the TxService registration into setup().

This suite-level beforeEach bypasses 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 of beforeEach.

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between 857104c and 41a22fb.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (42)
  • apps/api/drizzle/0032_melodic_onslaught.sql
  • apps/api/drizzle/0033_x402_deploy_columns.sql
  • apps/api/drizzle/meta/0032_snapshot.json
  • apps/api/drizzle/meta/0033_snapshot.json
  • apps/api/drizzle/meta/_journal.json
  • apps/api/package.json
  • apps/api/src/app/providers/jobs.provider.ts
  • apps/api/src/auth/services/ability/ability.service.ts
  • apps/api/src/billing/config/env.config.spec.ts
  • apps/api/src/billing/config/env.config.ts
  • apps/api/src/billing/config/x402-networks.ts
  • apps/api/src/billing/controllers/x402/x402.controller.spec.ts
  • apps/api/src/billing/controllers/x402/x402.controller.ts
  • apps/api/src/billing/events/x402-reconcile-settled.ts
  • apps/api/src/billing/http-schemas/x402.schema.ts
  • apps/api/src/billing/model-schemas/index.ts
  • apps/api/src/billing/model-schemas/x402-transaction/x402-transaction.schema.ts
  • apps/api/src/billing/repositories/index.ts
  • apps/api/src/billing/repositories/x402-transaction/x402-transaction.repository.ts
  • apps/api/src/billing/routes/index.ts
  • apps/api/src/billing/routes/x402/x402.router.ts
  • apps/api/src/billing/services/x402-reconcile-job/x402-reconcile-job.service.spec.ts
  • apps/api/src/billing/services/x402-reconcile-job/x402-reconcile-job.service.ts
  • apps/api/src/billing/services/x402-reconcile-settled/x402-reconcile-settled.handler.spec.ts
  • apps/api/src/billing/services/x402-reconcile-settled/x402-reconcile-settled.handler.ts
  • apps/api/src/billing/services/x402/x402-error-codes.ts
  • apps/api/src/billing/services/x402/x402-http-server-factory.service.ts
  • apps/api/src/billing/services/x402/x402.service.spec.ts
  • apps/api/src/billing/services/x402/x402.service.ts
  • apps/api/src/routers/open-api-handlers.ts
  • doc/x402/PLAN.md
  • doc/x402/RELEASE-NOTES-v1.0.md
  • doc/x402/RELEASE-NOTES-v1.1.md
  • doc/x402/RELEASE-NOTES-v1.2.md
  • doc/x402/RELEASE-NOTES-v2.0.md
  • doc/x402/STATE.md
  • doc/x402/USAGE.md
  • examples/x402/.env.example
  • examples/x402/README.md
  • examples/x402/package.json
  • examples/x402/top-up.ts
  • examples/x402/tsconfig.json

Comment on lines +47 to +53
402: {
description: "Payment required or payment invalid/failed to settle",
content: {
"application/json": {
schema: X402PaymentRequiredResponseSchema
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.

Comment on lines +162 to +163
const amountUsdCents = Math.round(amountUsd * 100);
const settlement = await this.settlePaymentAndCredit(context, { userId, amountUsdCents }, verified);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.

Comment on lines +224 to +253
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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

Comment on lines +360 to +363
const existing = await this.x402TransactionRepository.findByPaymentHash(paymentHash);
const resolvedExisting = await this.resolveTerminalOrSettled(existing, cancellationDispatcher);
if (resolvedExisting) {
return resolvedExisting;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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

Comment on lines +366 to +381
// `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 });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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

Comment on lines +419 to +427
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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant