Skip to content

feat(x402): EIP-3009 "exact" signing helper for agent payments#12

Merged
yakimoto merged 2 commits into
mainfrom
feat/x402-exact-signing
Jun 7, 2026
Merged

feat(x402): EIP-3009 "exact" signing helper for agent payments#12
yakimoto merged 2 commits into
mainfrom
feat/x402-exact-signing

Conversation

@yakimoto
Copy link
Copy Markdown
Contributor

@yakimoto yakimoto commented Jun 7, 2026

What

Adds wave.x402 — the EIP-3009 "exact" scheme signing helper so a non-JS agent can pay a WAVE x402 facilitator (gateway.wave.online). It signs a USDC TransferWithAuthorization as EIP-712 typed data and builds the X-Payment header — the same flow the TS @wave-av/agent-money helper provides. Closes the last open piece of the facilitator GTM (#110) for Python.

from wave.x402 import sign_exact_authorization, encode_exact_payment_header
payload = sign_exact_authorization(
    private_key=PAYER_KEY, network="base",
    to=req["payTo"], value=req["maxAmountRequired"],
    valid_before=int(time.time()) + 600,
)
resp = httpx.get(url, headers={"X-Payment": encode_exact_payment_header("base", payload)})

Byte-for-byte compatible with the facilitator

The WAVE facilitator's /verify recovers the payer from this exact EIP-712 hash. To guarantee the Python signer matches the reference viem stack, this PR commits a canonical cross-language conformance vector (tests/fixtures/x402_exact_vector.json) — a fixed key + fixed nonce → exact signature + header, generated from viem — and asserts byte-identity in tests/test_x402.py.

Verified locally against viem for both networks:

network signature
base 0xbb7a2113…a66c9171c
base-sepolia 0x2e8a2041…b0b7dedb01c

…and the base64 X-Payment header matches exactly. This vector is intended to be the shared fixture every future port (Go/Rust/Ruby) reproduces.

Changes

  • wave/x402.pyNETWORKS (base + base-sepolia USDC EIP-712 domains: mainnet "USD Coin" vs Sepolia "USDC", both version "2"), random_nonce, get_network_config, sign_exact_authorization, encode_exact_payment_header. eth-account is lazy-imported, so importing the module is dependency-free; calling sign_* without the extra raises a clear ImportError.
  • pyproject.toml[project.optional-dependencies] x402 = ["eth-account>=0.10.0"].
  • tests/ — the conformance fixture + 9 tests (sig/header byte-identity ×2 networks, payer derivation, int→string coercion, nonce format, from-mismatch + unsupported-network guards).
  • wave/__init__.py — exports sign_exact_authorization + encode_exact_payment_header.

Notes

  • The test key is the universally-published Hardhat account ci(F6): govern this public mirror — foundation-gate + CODEOWNERS #1 key (the same one already used in wave-gateway tests) — not a secret, never for real funds.
  • Verification: 9/9 pytest ([x402,dev] extras), ruff check clean. (test_version/test_api_count in test_sdk_exports.py already fail on main — version drift 2.1.0 vs 2.0.0 and a Wave-class attr count — unrelated to this PR and not run in CI's ruff-only lint workflow.)

🤖 Generated with Claude Code


Note

Medium Risk
New payment-signing path handles private keys and must match facilitator EIP-712 bytes exactly; mistakes would break verification but scope is isolated to optional client signing, not server auth.

Overview
Adds client-side x402 "exact" payments for Python agents: a new wave.x402 module signs USDC EIP-3009 TransferWithAuthorization (EIP-712) and builds the base64 X-Payment header for the WAVE facilitator, mirroring @wave-av/agent-money for base and base-sepolia.

Signing is gated behind optional pip install "wave-sdk[x402]" (eth-account); the module imports without that extra and sign_exact_authorization raises a clear ImportError when signing is attempted. sign_exact_authorization and encode_exact_payment_header are re-exported from wave.

Conformance is locked with tests/fixtures/x402_exact_vector.json (fixed key/nonce → signature + header) and tests/test_x402.py asserts byte-identical output for both networks, plus guards for payer derivation, nonce format, and invalid network/from mismatches.

Reviewed by Cursor Bugbot for commit 4f99f8b. Configure here.


Summary by cubic

Adds wave.x402 — an EIP-3009 “exact” signing helper to let non-JS agents pay the WAVE x402 facilitator by signing USDC TransferWithAuthorization and building the X-Payment header. Byte-for-byte compatible with WAVE’s reference TypeScript x402 signer for base and base-sepolia. Docs updated to remove private repo references.

  • New Features

    • sign_exact_authorization produces an EIP-712 signature bound to the payer wallet.
    • encode_exact_payment_header builds the base64 X-Payment envelope.
    • random_nonce and get_network_config helpers; networks: base, base-sepolia.
    • Added a shared conformance vector and tests asserting exact signature/header matches.
  • Dependencies

    • Optional extra: x402 = ["eth-account>=0.10.0"]. Install with: pip install "wave-sdk[x402]".

Written for commit edd663b. Summary will update on new commits.

Review in cubic

wave.x402.sign_exact_authorization + encode_exact_payment_header let a
non-JS agent pay a WAVE x402 facilitator (gateway.wave.online): sign a
USDC TransferWithAuthorization as EIP-712 typed data, then build the
X-Payment header. Byte-for-byte compatible with the reference TS helper
in @wave-av/agent-money (the same viem stack the facilitator verifies).

- wave/x402.py: NETWORKS (base + base-sepolia USDC domains), random_nonce,
  get_network_config, sign_exact_authorization, encode_exact_payment_header.
  eth-account is an OPTIONAL extra (lazy-imported) -> the module import is
  dependency-free.
- pyproject: [project.optional-dependencies] x402 = ["eth-account>=0.10.0"].
- tests/fixtures/x402_exact_vector.json: canonical cross-language
  conformance vector (fixed key + nonce -> exact sig/header) that every
  WAVE SDK port must reproduce. Generated from the viem reference.
- tests/test_x402.py: asserts byte-identity vs the vector (both networks),
  payer derivation, nonce format, from-mismatch + unsupported-network guards.
- __init__: export sign_exact_authorization + encode_exact_payment_header.

Verified locally: signatures + headers byte-identical to viem for base and
base-sepolia; 9/9 pytest; ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jun 7, 2026

⚠️ No Changeset found

Latest commit: edd663b

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 7, 2026

Warning

Review limit reached

@yakimoto, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 7 minutes and 45 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2314ad60-54ee-4627-9d5a-dacc6a4c516e

📥 Commits

Reviewing files that changed from the base of the PR and between 2b6a6ee and edd663b.

📒 Files selected for processing (5)
  • pyproject.toml
  • tests/fixtures/x402_exact_vector.json
  • tests/test_x402.py
  • wave/__init__.py
  • wave/x402.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/x402-exact-signing
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/x402-exact-signing

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

@socket-security
Copy link
Copy Markdown

socket-security Bot commented Jun 7, 2026

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

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedeth-account@​0.13.7100100100100100

View full report

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 5 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.
Architecture diagram
sequenceDiagram
    participant Agent as Python Agent
    participant SDK as wave.x402
    participant Account as eth_account (optional)
    participant Facilitator as WAVE Facilitator (gateway.wave.online)
    participant USDC as USDC Contract (chain)

    Note over Agent,Facilitator: NEW: EIP-3009 "exact" scheme payment flow

    Agent->>SDK: sign_exact_authorization(private_key, network, to, value, valid_before)
    SDK->>SDK: Lazy import eth_account
    alt eth_account not installed
        SDK-->>Agent: Raise ImportError with install hint
    end
    SDK->>SDK: Resolve network config (base/base-sepolia)
    alt Unsupported network
        SDK-->>Agent: Raise ValueError
    end
    SDK->>SDK: Derive payer address from private key
    alt from_address mismatch
        SDK-->>Agent: Raise ValueError
    end
    SDK->>SDK: Generate or validate nonce (0x + 64 hex)
    SDK->>Account: encode_typed_data(domain, message_types, message_data)
    Account-->>SDK: Signable message hash
    SDK->>Account: sign_message(signable)
    Account-->>SDK: ECDSA signature (0x + 130 hex chars)
    SDK-->>Agent: { signature, authorization: { from, to, value, validAfter, validBefore, nonce } }

    Agent->>Agent: Call encode_exact_payment_header(network, payload)
    Agent->>SDK: encode_exact_payment_header(network, payload)
    SDK->>SDK: Build envelope: { x402Version: 1, scheme: "exact", network, payload }
    SDK->>SDK: base64(JSON.stringify(envelope))
    SDK-->>Agent: Base64-encoded X-Payment header string

    Agent->>Facilitator: GET resource_url (X-Payment: <base64 header>)
    Facilitator->>Facilitator: Decode header, verify EIP-712 signature
    Facilitator->>USDC: TransferWithAuthorization (on-chain pull)
    USDC-->>Facilitator: Transfer confirmed
    Facilitator-->>Agent: Resource access granted

    Note over Agent,Facilitator: Canonical conformance vectors (test suite)
    Agent->>SDK: sign_exact_authorization with fixed key/nonce
    SDK-->>Agent: Signature + header
    Agent->>Agent: Assert byte-identical to vector.json
Loading

Re-trigger cubic

…ate)

The public-repo content-policy gate (GUARD_PRIVATE_REPOS) blocks naming the
private agent-money repo in this open-core SDK. Replace those references with
neutral "WAVE's reference TypeScript x402 signer" phrasing; no behaviour change
(signatures still byte-identical to the conformance vector; 9/9 tests, ruff clean).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@yakimoto yakimoto merged commit 3065203 into main Jun 7, 2026
13 checks passed
@yakimoto yakimoto deleted the feat/x402-exact-signing branch June 7, 2026 17:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant