Repository navigation
Description
Why
The flagship: the agent hits HTTP 402, Deckard validates the payment demand against policy, signs an EIP-3009 TransferWithAuthorization (EIP-712), the agent retries and gets the resource — keys never leave the machine, the client pays no gas (the settler does), and the facilitator cannot alter amount or destination (worst case is non-settlement, never theft). Self-custodial, policy-bounded machine payments on Ethereum.
Canonical scheme spec: https://github.com/x402-foundation/x402/blob/main/specs/schemes/exact/scheme_exact_evm.md · protocol v2: https://github.com/x402-foundation/x402/blob/main/specs/x402-specification-v2.md
Corrected 2026-07-10 (codebase re-verified): #34 is the x402/MPP linchpin — the whole track (#207/#208/#209/#210) is gated on it. It is a LARGE net-new build but fully unblocked and de-risked: every stated dependency is on
main, and spike #206 (PR #211) proved the exact crypto byte-for-byte. Spike-forced corrections and a split/sequencing plan added below.
Ready to start — dependencies all landed (verified 2026-07-10)
- Wire-contract evolution: Hello capability discovery + the five evolution rules #31 capability mechanism —
capabilities.rswith a documentedCAP_extension point. ✔ - Freeze the Deny-reason vocabulary in deckard-contract #28 frozen Deny vocabulary —
deny_reasons.rs+ typed tags. ✔ - The daily-cap + UTC-midnight-rollover ledger —
signerd/spend_store.rsSpendStore(reserve/commit/release),daemon.rs current_utc_day()/rollover(). ✔ But note: it is a single global counter keyed by(chain_id, account, day)withcommitted_wei/reserved_wei. x402 phase 2: exact/EIP-3009 payments on the Ethereum reference path, fully local demo loop #34's per-assettoken_budgetsis a net-new generalization — the reserve/commit/release pattern is reusable (it is exactly the "signed-but-unsettled counts as spent, retry reuses" primitive), the data structure is new (per-asset keyed). - Mainnet guardrail —
chain.rs::is_testnet_or_dev+daemon.rs guardrail_active(). ✔
Spike-forced corrections (from #206 / PR #211 — the spec had these wrong)
- Use protocol v2 / CAIP-2 network ids; emit v2, decode v1 and v2.
- EIP-712 domain name is
"USDC"(version"2"), NOT"USD Coin". - The KAT lives as a JSON fixture (
docs/research/x402-spike/eip3009-kat.json) — re-encode it as a Rust#[test]inside this issue (digest0x89d2449c…, sig0x3e1236af…). propose_paymentmust copy the Sendproposeauto-allow-within-budget + guardrail-downgrade path (daemon.rs:683), NOT the always-NeedsApprovalpropose_messagepath.- Do not sign as an anvil default account (EIP-7702 delegation routes USDC to EIP-1271 and rejects) — the spike
run.shdocuments the working setup. - The
x402-rsfacilitator binary is a demo prerequisite surfaced bydemo-check(ajust demosibling process on the fork), never a build dependency.
In scope
-
deckard-core: hand-rolled EIP-3009TransferWithAuthorizationviaalloy sol!mirroringcow_types.rs:26-59+eip712_signing_hashsign + the Rust KAT. (No wire files — this PR can run in parallel with the browser milestone.) -
deckard-contract:PaymentRequirementsv1/v2 decode +ProposePayment/SignPaymentwire variants +CAP_X402_EXACT. (Wire files — rebase this on Connections: daemon-held dapp sessions, listed in the app, revocable per site #199'sSignerRequestdelta; land one enum change first, then the other. See Connections: daemon-held dapp sessions, listed in the app, revocable per site #199 "Wire sequencing".) - policy: per-asset
token_budgets(per-tx + daily + spent-today, UTC rollover) +asset_allow(default-deny) +max_validity_secs+evaluate_paymentuntrusted-input checks (asset ∈ allow, amount ≤ budget, payTo rendered, validity ≤ max, network == chain). Any miss → typed Deny (Freeze the Deny-reason vocabulary in deckard-contract #28). Mainnet guardrail unchanged. - daemon:
propose_payment(auto-allow-within-budget + guardrail-downgrade) + per-asset SpendStore + bearer-nonce idempotent retry-reuse. - MCP:
deckard_x402_sign, capability-gated. - Demo fixtures (toy 402 server +
x402-rsfacilitator on the fork) +demo-checkprobes. - Acceptance X3–X10 headless against the fixtures (within-budget pay → settle; hostile asset → Deny; amount > budget → Deny; validity > max → Deny; retry reuses same auth; daily budget exhausted → Deny until rollover; chain-1 → NeedsApproval; transcript scan clean).
Suggested split (3 PRs)
- core (EIP-3009 struct + sign + KAT) — no wire files, parallelizable with browser work.
- policy + daemon + wire — rebases on Connections: daemon-held dapp sessions, listed in the app, revocable per site #199's enum delta.
- mcp + fixtures + demo-check.
Not in scope
upto/batch-settlement, Permit2, Base/hosted facilitators, the private Railgun leg + ERC-7710 settlement, general ERC-20 beyond the allowlisted demo asset.
Definition of done
- All in-scope items runnable headless; X3–X10 green.
-
cargo fmt --all --checkclean;just checkgreen (both feature configs);cargo test --workspacegreen. - No new deps (hand-roll the x402 structs —
alloy-sol-typesis already compiled forcow_types); anything else approved before merge. - THREAT-MODEL.md delta merged (typed-data signing surface; bearer-authorization residual risk).
Ordering
Unblocked today. #32 (sign-in) is NOT a blocker (its message seam already shipped — see #32). Sequence the x402 track: #34 → #208 → #209 (+#207 for GUI beats); #210 is a near-free follow-on to #208.
Activity
hellno commentedon Jun 19, 2026
⛓ Blocked by #31 and #32 — adds PaymentAuthorization (needs #31's evolution rules); x402 phase 2 follows phase 1 sign-in (#32).
hellno commentedon Jul 7, 2026
Spike #206 decision — build shape + local loop (evidence-backed)
Ran the #206 spike (throwaway; recipe in #211 →
docs/research/x402-spike/). Both questions answered with a running loop, not just reading. TL;DR: hand-roll the EIP-3009 struct indeckard-core, and build the demo on the existing Ethereum Sepolia fork. Details below, plus the spec corrections v2 forces on this issue.Decision 1 — hand-roll the typed data, do not depend on
x402-types/x402-reqwestBuilt the same
TransferWithAuthorizationauthorization two ways and compared byte-for-byte:Four independent implementations agree on that digest+sig: deckard's hand-rolled
alloy sol!, x402-rs'sTransferWithAuthorization, foundry'scast wallet sign --data, and the deployed USDC contract itself (it accepted the hand-rolled signature during/settle). The typehash equals the contract's ownTRANSFER_WITH_AUTHORIZATION_TYPEHASH().Why hand-roll wins: x402-rs's type is literally a 6-field
sol!struct +eip712_signing_hash— the exact patterndeckard-core/src/cow_types.rsalready ships for CoW orders (a harder struct). To get that type from x402 you'd depend onx402-chain-eip155, which drags 222 transitive crates (3×rustls,hyper,reqwest,tokio,tower, the fullalloy-provider/rpc-client/transport-http/contractstack) into the#![forbid(unsafe_code)]trust core — a networked payments client behind the signer boundary — for zero cryptographic gain.x402-typesalone does not even carry the signing struct (only the JSON wire structs, which are agent-side transport). It also fights our lints (clientusesrand::rng(); we denythread_rng).Concretely for this issue:
sol!TransferWithAuthorizationtodeckard-corenext tocow_types; KAT-gate it againstdocs/research/x402-spike/eip3009-kat.json(domain, message, typehash, digest, signature — same discipline as the cow_types ethers fixture).sol!+eip712_signing_hash. No general typed-data path.demo-check(build:cargo build -p x402-facilitator --no-default-features --features chain-eip155, ~90s, no Docker), never a build dependency. DoD rule 4 stays satisfied: no new deps in the trust core.X-Payment(v1) andPayment-Required/Payment-Signature(v2) parsing is agent-side — the facilitator API is plain JSON in both versions. Deckard receives the decodedPaymentRequirementsand returns a signed authorization; it never touches an HTTP header. Thedeckard_fetch_paidclient (Agent-side x402 client: detect 402 → policy check → pay → retry #208) / x402-reqwest owns both.Decision 2 — the fully-local loop is PROVEN; stay on the Ethereum Sepolia fork
402 → sign EIP-3009 → /verify → /settle → on-chainruns end-to-end against real Circle USDCFiatTokenV2_2onanvil --fork-url $RPC_URL_SEPOLIA --fork-block-number 10822990with a self-hosted x402-rs facilitator (eip155:11155111 → fork). Cold-startrun.shasserts:Ethereum Sepolia (
11155111) is not in x402-rs'sKNOWN_NETWORKS(confirming no hosted facilitator serves it), but itsChainIdaccepts arbitraryeip155:*, so a self-hosted config just works. Go/no-go: stay on the Sepolia fork — single strongest reason: it's the demo world every Deckard demo already runs, it carries genuine EIP-3009 bytecode, and the loop is green on it with zero new infra. A Base fork would buy only hosted-facilitator parity, which is irrelevant to a self-hosted local demo; keep Base for the later hosted-reach phase this issue already defers.Spec corrections this issue must absorb (v2)
"base-sepolia"); Ethereum Sepolia has no v1 name, so the fork path must be v2 (network: "eip155:11155111"). ThePaymentAuthorization.requirementsdecoder should accept v1 and v2 shapes (as this issue says) but the demo runs v2."USDC", version"2"on this deployment — not mainnet's"USD Coin". Verified against on-chainDOMAIN_SEPARATOR(). The untrustedPaymentRequirementscarries the domain inextra: {assetTransferMethod:"eip3009", name, version}; the daemon must sign with the domain from the requirements it validated, andnetwork-match must pin chainId 11155111 — do not hardcode the mainnet domain name.amount(not v1maxAmountRequired), CAIP-2network,payTo,maxTimeoutSeconds,asset,extra. Facilitator request body:{ x402Version, paymentPayload{ accepted, payload{ signature, authorization{from,to,value,validAfter,validBefore,nonce} }, x402Version }, paymentRequirements }. Amounts/timestamps are decimal strings.0xf39F…/0x7099…/0x3C44…carry EIP-7702 delegation designators (code 0xef0100…) on real Sepolia, so USDC'sSignatureCheckerroutes them to EIP-1271 and rejects the EOA signature. Use fresh codeless keys and assertcast code $BUYER == 0x. The demo buyer funds viaanvil_setStorageAton balance slot 9.Full write-up, transcripts, config, and the KAT: #211 →
docs/research/x402-spike/.feat(wire): #31 · Hello capability discovery + the five evolution rul…
docs(x402): spike #206 — hand-roll verdict + proven local EIP-3009 fa…
hellno commentedon Jul 7, 2026
Owner ratified (2026-07-07): both spike decisions are confirmed for implementation —
TransferWithAuthorizationindeckard-coreviaalloy sol!+eip712_signing_hash(thecow_typesprecedent), KAT-gated againstdocs/research/x402-spike/eip3009-kat.json. Nox402-types/x402-chain-eip155dependency in the trust core; x402-rs stays a demo-fixture facilitator binary.Full rationale + evidence: the decision comment above and
docs/research/x402-spike/(merged in #211).