Skip to content

Commit 04c5a6a

Browse files
whoabuddyclaude
andauthored
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

.planning/2026-04-22-boring-tx-state-machine/phases/01-research/NOTES.md

Lines changed: 575 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<plan>
3+
<goal>
4+
Produce a written map from the current x402-api compat-shim code paths to the native
5+
boring-tx events the relay now emits, plus a port plan for the polling DO pattern from
6+
landing-page and agent-news. Output: NOTES.md with shim inventory, behavior table,
7+
tx-schemas entry points, relay endpoint shapes, DO API sketch, and risk list.
8+
</goal>
9+
10+
<context>
11+
x402-api v1.6.1 uses x402-stacks ^2.0.1 and @aibtc/tx-schemas ^0.3.0 (installed: 1.0.0).
12+
The middleware at src/middleware/x402.ts calls verifier.settle() from x402-stacks.
13+
All payment event logs carry compat_shim_used: true, paymentId: null,
14+
checkStatusUrl_present: false because extractCanonicalPaymentDetails() falls through to
15+
the "inferred" path — the relay never returned paymentId/checkStatusUrl before boring-tx.
16+
17+
x402-sponsor-relay v1.30.1 now generates paymentId (pay_ prefix) in submitPayment() and
18+
always populates checkStatusUrl on every response. The compat shim path (inferLegacyStatus,
19+
inferLegacyTerminalReason) is needed only for relay responses that predate boring-tx.
20+
21+
Reference repos (read-only):
22+
- landing-page: lib/inbox/payment-contract.ts, lib/inbox/x402-verify.ts
23+
- agent-news: src/routes/payment-status.ts, src/services/x402.ts
24+
- tx-schemas: src/core/*, src/http/*, src/rpc/*
25+
- x402-sponsor-relay: src/rpc.ts (submitPayment, checkPayment), src/endpoints/payment-status.ts
26+
</context>
27+
28+
<task id="1">
29+
<name>Read core source files and reference repos</name>
30+
<files>
31+
src/middleware/x402.ts,
32+
src/utils/payment-status.ts,
33+
src/utils/payment-observability.ts,
34+
src/utils/payment-contract.ts,
35+
src/types.ts,
36+
src/durable-objects/UsageDO.ts,
37+
wrangler.jsonc,
38+
package.json,
39+
~/dev/aibtcdev/tx-schemas/src/core/enums.ts,
40+
~/dev/aibtcdev/tx-schemas/src/core/terminal-reasons.ts,
41+
~/dev/aibtcdev/tx-schemas/src/core/payment.ts,
42+
~/dev/aibtcdev/tx-schemas/src/http/schemas.ts,
43+
~/dev/aibtcdev/tx-schemas/src/rpc/schemas.ts,
44+
~/dev/aibtcdev/x402-sponsor-relay/src/rpc.ts,
45+
~/dev/aibtcdev/x402-sponsor-relay/src/endpoints/payment-status.ts,
46+
~/dev/aibtcdev/agent-news/src/services/x402.ts,
47+
~/dev/aibtcdev/agent-news/src/routes/payment-status.ts
48+
</files>
49+
<action>
50+
Read all listed files to understand:
51+
1. Exactly where compat-shim flags are set and logged in x402-api
52+
2. What fields the relay now emits (paymentId, checkStatusUrl, status, terminalReason)
53+
3. What tx-schemas exports are available under @aibtc/tx-schemas/{core,http,rpc}
54+
4. How agent-news verifyPayment() + payment-status route implement the polling DO
55+
5. What the relay RPC interface looks like (submitPayment, checkPayment signatures)
56+
</action>
57+
<verify>
58+
All files readable without error. Key values extracted:
59+
- current installed tx-schemas version
60+
- compat shim code locations (file:line)
61+
- relay checkStatusUrl URL pattern
62+
- agent-news DO polling implementation skeleton
63+
</verify>
64+
<done>
65+
Complete inventory of all compat-shim touch-points and relay native fields.
66+
</done>
67+
</task>
68+
69+
<task id="2">
70+
<name>Write NOTES.md with all required sections</name>
71+
<files>
72+
.planning/2026-04-22-boring-tx-state-machine/phases/01-research/NOTES.md
73+
</files>
74+
<action>
75+
Create NOTES.md covering all six required sections:
76+
1. Shim inventory - every file, function, and log field carrying compat_shim semantics
77+
2. Behavior comparison table - landing-page vs agent-news vs x402-api
78+
3. tx-schemas entry points - which exports to use in each phase
79+
4. Relay endpoint/response shapes - submitPayment and checkPayment exact types
80+
5. DO public API sketch - concrete TypeScript interface and SQLite schema for PaymentPollingDO
81+
6. Risk list - versioning, #87 coupling, data migration, wrangler migration tag
82+
83+
The DO public API sketch must be concrete enough to implement from (Phase 4 deliverable).
84+
Include the swap point comment at poll() as described in PHASES.md.
85+
</action>
86+
<verify>
87+
NOTES.md exists, has all 6 headings, is not a placeholder, DO sketch includes
88+
TypeScript interface with track/poll/status/derivedHints methods and SQLite CREATE TABLE.
89+
</verify>
90+
<done>
91+
NOTES.md written with substantive content in every section.
92+
</done>
93+
</task>
94+
95+
<task id="3">
96+
<name>Commit PLAN.md and NOTES.md</name>
97+
<files>
98+
.planning/2026-04-22-boring-tx-state-machine/phases/01-research/PLAN.md,
99+
.planning/2026-04-22-boring-tx-state-machine/phases/01-research/NOTES.md
100+
</files>
101+
<action>
102+
Stage both files and commit with message:
103+
docs(planning): phase 1 research notes for boring-tx adoption
104+
</action>
105+
<verify>
106+
git log shows the commit with both files.
107+
</verify>
108+
<done>
109+
Conventional commit in git history.
110+
</done>
111+
</task>
112+
</plan>
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Phase 2: Branch Setup + Deps
2+
3+
Date: 2026-04-22
4+
Phase: 02-deps
5+
6+
## Goal
7+
8+
Fresh feature branch `feat/boring-tx-state-machine` off latest `origin/main` with
9+
`@aibtc/tx-schemas` bumped to `^1.0.0`. Baseline `npm run check` and
10+
`npm run deploy:dry-run` clean BEFORE any logic changes.
11+
12+
## What Was Done
13+
14+
### Branch Setup
15+
16+
- Local `main` had diverged from `origin/main` (local had Phase 1 planning commit,
17+
origin/main had 4 newer commits including `e6ba205`, `457e955`, `ed24545`, `46b8693`)
18+
- Stashed `tests/_shared_utils.ts` (Phase 7 staged changes) to keep a clean working tree
19+
- Fetched `origin/main` (latest: `46b8693 chore(main): release 1.6.1`)
20+
- Created `feat/boring-tx-state-machine` off `origin/main`
21+
- Cherry-picked Phase 1 planning commit (`c1b46b5``f881569` on feature branch)
22+
- Popped stash to restore `tests/_shared_utils.ts` modification
23+
24+
### Dependency Bump
25+
26+
Phase 1 NOTES.md identified (R1 in Risk List):
27+
- Installed version: `@aibtc/tx-schemas@1.0.0`
28+
- package.json constraint: `^0.3.0` — this does NOT resolve 1.0.0 for 0.x.y semver locking
29+
- Latest published on npm: `1.0.0`
30+
- Action required: bump constraint to `^1.0.0`
31+
32+
Updated `package.json`:
33+
```
34+
"@aibtc/tx-schemas": "^0.3.0" → "@aibtc/tx-schemas": "^1.0.0"
35+
```
36+
37+
Ran `npm install``@aibtc/tx-schemas@1.0.0` resolved correctly.
38+
39+
### Patch File Removal (Mechanical Cleanup)
40+
41+
During `npm install`, `patch-package` (postinstall hook) errored applying
42+
`patches/x402-stacks+2.0.1.patch`. Investigation showed the patch changes
43+
(bump HTTP client timeout from 30000ms/15000ms to 120000ms) are now incorporated
44+
upstream in x402-stacks itself — both `dist/verifier-v2.js` and `dist/verifier.js`
45+
already have `timeout: 120000`. The patch file was stale.
46+
47+
Removed: `patches/x402-stacks+2.0.1.patch`
48+
49+
This is a mechanical cleanup — no behavioral change (timeout is 120000ms before and
50+
after). The installed package already contains the desired timeout value.
51+
52+
### Import Path Verification
53+
54+
No import-path changes were required. The existing import in
55+
`src/utils/payment-contract.ts` uses `@aibtc/tx-schemas/core` sub-path:
56+
```ts
57+
import { CanonicalDomainBoundary, PAYMENT_STATES } from "@aibtc/tx-schemas/core";
58+
```
59+
This sub-path was present in v0.3.0 and remains available in v1.0.0 — no breakage.
60+
61+
### Baseline Verification
62+
63+
- `npm run check` (tsc --noEmit): exits 0, no errors
64+
- `npm run deploy:dry-run`: builds successfully (1027.76 KiB / gzip: 272.26 KiB)
65+
- `tests/_shared_utils.ts`: still shows as modified, not committed (preserved for Phase 7)
66+
67+
## Files Changed in Phase 2 Commit
68+
69+
| File | Change |
70+
|------|--------|
71+
| `package.json` | `@aibtc/tx-schemas` constraint `^0.3.0``^1.0.0` |
72+
| `package-lock.json` | Lockfile update for tx-schemas resolution |
73+
| `patches/x402-stacks+2.0.1.patch` | Deleted (stale patch, fix now upstream) |
74+
| `.planning/…/phases/02-deps/PLAN.md` | This file |
75+
76+
## Not Included in Commit
77+
78+
- `tests/_shared_utils.ts` — preserved for Phase 7 (NonceTracker + signPaymentWithNonce)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Phase 9: Verify — Checklist & Results
2+
3+
Date: 2026-04-22
4+
5+
## Verification Checklist
6+
7+
### 1. Working Tree Status
8+
- [x] `git status` — clean
9+
- Only untracked `.claude/` dir (not in src/, tests/, or config)
10+
- No staged or modified files
11+
12+
### 2. Type Check
13+
- [x] `npm run check`**0 errors**
14+
- `tsc --noEmit` exits cleanly with no output
15+
16+
### 3. Deploy Dry-Run
17+
- [x] `npm run deploy:dry-run`**clean build**
18+
- Total Upload: 1031.33 KiB / gzip: 273.31 KiB
19+
- `PAYMENT_POLLING_DO (PaymentPollingDO)` binding confirmed present
20+
- All 4 Durable Objects wired: UsageDO, StorageDO, MetricsDO, PaymentPollingDO
21+
- Only expected warning: multiple environments defined, no target specified (non-blocking)
22+
23+
### 4. Quick E2E Tests (npm test)
24+
- [x] `npm test`**14/14 passed (100.0%)**
25+
- Mode: quick, Tokens: STX, Server: https://x402.aibtc.dev
26+
- Categories: hashing (6), stacks (6), inference (2)
27+
- All stateless endpoints pass
28+
29+
### 5. Full E2E Tests (npm run test:full)
30+
- [ ] `npm run test:full`**SKIPPED: X402_CLIENT_PK not set in env**
31+
- Note from Phase 7: test:full against live staging would fail
32+
`payment-polling-lifecycle` on X-PAYMENT-ID assertion because the
33+
new header is deployment-gated (not yet deployed). This is expected.
34+
- Path to verify: `npm run dev` (local), then
35+
`X402_WORKER_URL=http://localhost:8787 npm run test:full`
36+
37+
### 6. Unit Tests
38+
- [x] `bun test tests/*.unit.test.ts`**114 passed, 0 failed**
39+
- 8 files, 340 expect() calls, 198ms
40+
- Files: cloudflare-ai-fallback, model-cache, openrouter-validation,
41+
payment-contract, payment-middleware, payment-observability,
42+
payment-polling-do, payment-status
43+
44+
### 7. Rebase on origin/main
45+
- [x] `git fetch origin` — clean
46+
- [x] `git rebase origin/main`**already up to date**
47+
- Merge base: `46b86936` (chore(main): release 1.6.2)
48+
- No rebase needed; branch was already cut from current main tip
49+
- [x] Post-rebase `npm run check` — clean (no change)
50+
- [x] Post-rebase `npm test` — 14/14 (no change)
51+
52+
### 8. Commits on Branch
53+
All 8 commits from Phase 1–8 land cleanly on origin/main:
54+
55+
```
56+
250bc32 refactor(payments): simplify post-boring-tx adoption
57+
f409653 test(payments): cover boring-tx lifecycle end-to-end
58+
1ab6e6f feat(payments): add retryable/retryAfter/nextSteps error hints
59+
c44f093 feat(payments): emit native payment.* events, drop compat shim
60+
7ac20c8 feat(payments): add PaymentPollingDO for checkStatusUrl polling
61+
5d218f7 refactor(payments): route payment types through @aibtc/tx-schemas
62+
7a70493 chore(deps): bump @aibtc/tx-schemas for boring-tx state machine
63+
f881569 docs(planning): phase 1 research notes for boring-tx adoption
64+
```
65+
66+
## Result
67+
68+
**PASS** — All blocking checks green. Branch is ready for PR (Phase 10).
69+
70+
Known non-blocking gap: `test:full` payment-polling-lifecycle needs deployed
71+
`X-PAYMENT-ID` header support on the relay side before it will pass against
72+
live staging. Local worker verification is the correct path and is noted in
73+
the PR body.

0 commit comments

Comments
 (0)