Commit 04c5a6a
feat(payments): adopt native boring-tx state machine (#107)
* docs(planning): phase 1 research notes for boring-tx adoption
Map from x402-api compat-shim code paths to the native boring-tx events
emitted by x402-sponsor-relay v1.30.1. Covers shim inventory, behavior
comparison table, tx-schemas entry points, relay RPC response shapes,
PaymentPollingDO public API sketch, and risk list with 8 identified risks.
Co-Authored-By: Claude <noreply@anthropic.com>
* chore(deps): bump @aibtc/tx-schemas for boring-tx state machine
Bump @aibtc/tx-schemas constraint from ^0.3.0 to ^1.0.0. The 0.x.y semver
locking meant the installed 1.0.0 package was outside the resolvable range;
pinning to ^1.0.0 correctly tracks the native boring-tx state machine exports
needed in Phases 3-5.
Also remove stale patches/x402-stacks+2.0.1.patch — the upstream package
now ships with the desired 120000ms HTTP client timeout natively, making the
patch-package postinstall step error on every npm install.
Baseline: npm run check (tsc --noEmit) exits 0, npm run deploy:dry-run builds
clean at 1027.76 KiB before any logic changes.
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(payments): route payment types through @aibtc/tx-schemas
Create src/services/payment-contract.ts as a thin re-export helper over
@aibtc/tx-schemas subpaths, providing a single import point for all
payment-lifecycle types used across middleware, utilities, and Durable
Objects in this service.
Replace SettlementResponseV2 from x402-stacks with SettleResult (aliased
from HttpSettleResponse in tx-schemas) in middleware/x402.ts and types.ts.
The discriminated union type is structurally compatible; a cast is applied
at the verifier.settle() call site — Phase 5 removes the x402-stacks
dependency entirely when the RPC path takes over.
extractCanonicalPaymentDetails is preserved (Phase 5 removes it).
Co-Authored-By: Claude <noreply@anthropic.com>
* feat(payments): add PaymentPollingDO for checkStatusUrl polling
Adds PaymentPollingDO (SQLite-backed Durable Object) that tracks in-flight
payments by polling checkStatusUrl with exponential backoff until terminal.
Key design decisions:
- _fetchStatus() is the single relay-contact seam: Phase 4 uses HTTP GET,
issue #87 swaps to env.X402_RELAY.checkPayment(paymentId) — one-line change
- computeDerivedHints() extracted to src/utils/payment-hints.ts so it is
testable with bun:test without requiring cloudflare:workers runtime
- DO instances are namespaced by paymentId (one per in-flight payment)
- Alarm backoff: 5s (polls 1-3), 15s (4-6), 60s (7+), 10-min timeout
Wiring:
- wrangler.jsonc: PAYMENT_POLLING_DO binding + v3 migration tag in all envs
- src/types.ts: Env interface updated with PAYMENT_POLLING_DO binding
- src/index.ts: exports class; mounts GET /payment-status/:paymentId (free)
Unit tests (36 passing):
- computeDerivedHints covers all terminal reason categories
- Stub-based track → poll → terminal happy-path flow
- derivedHints per category verified
Co-Authored-By: Claude <noreply@anthropic.com>
* feat(payments): emit native payment.* events, drop compat shim
Middleware now mints a client-side paymentId ("pay_" + crypto.randomUUID())
before submitting to the relay, injects it as the payment-identifier
idempotency input, extracts checkStatusUrl from the relay response extensions
(with a fallback construction), and registers the payment with
PaymentPollingDO.track() as fire-and-forget.
Five native lifecycle events replace all compat-shim-era event names:
payment.initiated — paymentId minted, about to submit to relay
payment.pending — relay acknowledged but payment is still in-flight
payment.confirmed — relay settled successfully
payment.failed — relay rejected with a terminal failure reason
payment.replaced — payment replaced by another tx (nonce race)
Deleted entirely:
- extractCanonicalPaymentDetails() and all internal shim helpers
- inferLegacyStatus() and inferLegacyTerminalReason()
- getRetryDecisionContext() (tests/_shared_utils.ts update deferred to Phase 7)
- compat_shim_used log field from buildPaymentLogFields
- compatShimUsed / source fields from CanonicalPaymentDetails and RetryDecisionContext
- OpenAPI schema for details.canonical in 402 response body
Unit tests updated to cover the three surviving classifier predicates and
the revised instability derivation signature.
Co-Authored-By: Claude <noreply@anthropic.com>
* feat(payments): add retryable/retryAfter/nextSteps error hints
All non-200 payment error responses now carry structured retry hints in
both the JSON body and the payment-response header (base64 JSON):
{ retryable, nextSteps, retryAfter? }
nextSteps tokens are stable identifiers tied to tx-schemas terminal
reason categories — not free-form prose — so clients can branch without
string-parsing:
rebuild_and_resign — sender nonce issue, build fresh tx
retry_later — transient relay/settlement error
start_new_payment — identity lost/replaced, restart x402 flow
fix_and_resend — invalid payload, fix before retrying
wait_for_confirmation — confirmed, delivery should proceed
classifyPaymentError() now checks canonical status (failed/replaced/
not_found) from the relay response before falling back to string
heuristics, so boring-tx relay responses are classified accurately.
Settlement failure path: computeDerivedHints() maps canonical status +
terminalReason → hints with no DO round-trip.
Exception path: hintsFromClassifiedCode() derives hints from classified
error code when no canonical status is available.
Updated llms-full.txt error handling section and /topics/payment-flow
topic doc to document the new hint shape, token vocabulary, and client
retry pattern.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(payments): cover boring-tx lifecycle end-to-end
Finish the pending tests/_shared_utils.ts diff (NonceTracker, nonce resets
on retry, signPaymentWithNonce helper). Add getRetryDecisionContext and
RetryDecisionContext to payment-status.ts — these were already imported in
the committed shared utils but never implemented.
Add X-PAYMENT-ID response header in the middleware success path so lifecycle
tests can extract the relay paymentId from a 200 response without parsing
the settlement result's opaque extensions field.
Add tests/payment-polling-lifecycle.test.ts (runPaymentPollingLifecycle):
- Makes a real x402 payment to /hashing/sha256
- Reads X-PAYMENT-ID from the 200 response header
- Polls GET /payment-status/:paymentId (free DO route) until terminal
- Asserts the DO snapshot has expected shape (paymentId, checkStatusUrl,
polledAt, pollCount, terminal status)
Register payment-polling in LIFECYCLE_RUNNERS in _run_all_tests.ts.
All 65 payment-*.unit.test.ts pass. npm test (quick mode, 14 stateless
endpoints) passes 14/14. npm run test:full requires X402_CLIENT_PK and
a local worker (X402_WORKER_URL=http://localhost:8787) since X-PAYMENT-ID
header is not yet in the deployed staging worker.
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(payments): simplify post-boring-tx adoption
Remove three dead imports from x402.ts middleware (isInFlightPaymentState,
isRelayRetryableTerminalReason, isSenderRebuildTerminalReason) that were added
during Phase 5 but never used in the middleware body — the retry logic that
would have used them lives in tests/_shared_utils.ts instead.
Clean up two stale phase-reference comments in payment-contract.ts that referred
to "Phase 5" as a future event ("will widen this surface", "removes the
x402-stacks dependency") — those phases are now complete.
TERMINAL_STATUSES duplication between payment-hints.ts and PaymentPollingDO.ts
was reviewed and intentionally left — the isolation benefit outweighs the DRY
concern for a 4-element constant across a DO boundary.
Co-Authored-By: Claude <noreply@anthropic.com>
* chore(planning): phase 9 verification log
All blocking checks pass: npm run check (0 errors), npm run deploy:dry-run
(clean, PAYMENT_POLLING_DO binding confirmed), npm test (14/14), bun unit
tests (114/114). Branch already on origin/main tip — no rebase needed.
Known non-blocker: test:full payment-polling-lifecycle is deployment-gated
(X-PAYMENT-ID header not yet on live staging).
Co-Authored-By: Claude <noreply@anthropic.com>
* chore(planning): phase 10 PR handoff
Record PR title, body, issue comment text, and all URLs for the
boring-tx state machine PR (#107) and related issue comments on
#94, #106, and #87.
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: re-trigger Workers Builds after migration bootstrap
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 46b8693 commit 04c5a6a
26 files changed
Lines changed: 2803 additions & 768 deletions
File tree
- .planning/2026-04-22-boring-tx-state-machine/phases
- 01-research
- 02-deps
- 09-verify
- 10-pr
- patches
- src
- durable-objects
- endpoints
- middleware
- services
- utils
- tests
Lines changed: 575 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 112 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
Lines changed: 78 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
Lines changed: 73 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
0 commit comments