Skip to content

feat(x402): migrate payment middleware to X402_RELAY RPC service binding #87

@whoabuddy

Description

@whoabuddy

Summary

x402-api still uses the synchronous HTTP /settle path via X402PaymentVerifier for payment verification. landing-page and agent-news have already migrated to the X402_RELAY service binding with the RelayRPC entrypoint, which routes payments through a queue-backed async path with retry semantics. The HTTP path has no queue retry and is directly exposed to the nonce conflict failures documented in x402-sponsor-relay#236.

Evidence

src/middleware/x402.ts:364-379 (current HTTP path):

// Verify payment with settlement relay using v2 API
const verifier = new X402PaymentVerifier(c.env.X402_FACILITATOR_URL);

log.debug("Settling payment via settlement relay", {
  relayUrl: c.env.X402_FACILITATOR_URL,
  expectedRecipient: c.env.X402_SERVER_ADDRESS,
  minAmount: paymentRequirements.amount,
  asset,
  network: networkV2,
});

let settleResult: SettlementResponseV2;
try {
  settleResult = await verifier.settle(paymentPayload, {
    paymentRequirements,
  });

wrangler.jsonc — services bindings (current, no X402_RELAY):

"services": [
  { "binding": "LOGS", "service": "worker-logs-staging", "entrypoint": "LogsRPC" }
]

Compare with landing-page/wrangler.jsonc:31-35 (migrated pattern):

{
  "binding": "X402_RELAY",
  "service": "x402-sponsor-relay",
  "entrypoint": "RelayRPC"
}

Root Cause

x402-api is the last first-party consumer still on the HTTP /settle path. This means:

  1. It is directly exposed to transient nonce conflict errors that the RPC queue path absorbs.
  2. Any /settle timeout or 5xx propagates as a payment failure to the end user with no retry.
  3. The migration path is well-established — landing-page completed it and serves as the reference implementation.

The 22 conflicting_nonce errors logged on 2026-03-26 all originated from x402-api hitting the HTTP path during cron test boundaries when a stale nonce was briefly in the pool.

Proposed Fix

1. Add X402_RELAY service binding to wrangler.jsonc

Add to the top-level services array and duplicate to each environment block:

{
  "binding": "X402_RELAY",
  "service": "x402-sponsor-relay-staging",  // staging env
  "entrypoint": "RelayRPC"
}
// production: "service": "x402-sponsor-relay"

2. Update src/middleware/x402.ts to use RPC

Replace the X402PaymentVerifier.settle() call with RPC:

// Before
const verifier = new X402PaymentVerifier(c.env.X402_FACILITATOR_URL);
settleResult = await verifier.settle(paymentPayload, { paymentRequirements });

// After
const relay = c.env.X402_RELAY;
settleResult = await relay.submitPayment(paymentPayload, { paymentRequirements });

The RPC path (RelayRPC.submitPayment) enqueues the settlement and returns a job ID. A companion checkPayment(jobId) poll or webhook completes the flow. This matches the pattern already working in landing-page.

3. Add X402_RELAY to TypeScript env type

Update src/types.ts (or wherever Env is declared) to add the X402_RELAY service binding type so TypeScript resolves the new binding.

Files

  • src/middleware/x402.ts:364-379 — replace X402PaymentVerifier.settle() with RPC call
  • wrangler.jsonc — add X402_RELAY service binding to top-level and both environments

Reference

  • landing-page migration: landing-page/wrangler.jsonc:31-35
  • Nonce conflict issue being mitigated: x402-sponsor-relay#236

Priority

Medium — aligns all first-party consumers on the same reliable path. The HTTP path continues to work but is exposed to reliability issues that the RPC path already handles.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestprod-gradeProduction-grade standards gap

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions