Skip to content

authCapture TS sdk#2308

Open
A1igator wants to merge 5 commits into
x402-foundation:mainfrom
BackTrackCo:aliabdoli/authCapture-evm-ts-sdk
Open

authCapture TS sdk#2308
A1igator wants to merge 5 commits into
x402-foundation:mainfrom
BackTrackCo:aliabdoli/authCapture-evm-ts-sdk

Conversation

@A1igator
Copy link
Copy Markdown
Contributor

@A1igator A1igator commented May 14, 2026

authCapture EVM Scheme — TypeScript SDK

At x402r.org, we've been working on refundable payments for x402. This PR is the TypeScript implementation of the authCapture scheme whose spec was merged in #1425. It builds on the audited base/commerce-payments contract stack, so no new contracts ship with this PR.

Scheme implementation: key design decisions

authCapture is single-shot and stateless. Each paid request signs one authorization (ERC-3009 or Permit2) carrying universal canonical collector + escrow addresses. The facilitator then submits a single tx that invokes AuthCaptureEscrow.authorize() or AuthCaptureEscrow.charge() (per extra.autoCapture), either calling escrow directly or routing through a passthrough captureAuthorizer contract.

Notable choices:

  • captureAuthorizer is merchant-set, never facilitator-injected — the facilitator's getExtra() returns undefined for this scheme. Per spec, captureAuthorizer is the only address allowed to call authorize / capture / void / refund / charge against the escrow, gated on-chain by onlySender(paymentInfo.operator). That address can be the facilitator's submitter EOA, or a smart contract that ultimately calls escrow as msg.sender (e.g., a refund arbiter). The facilitator advertises its submitter address via getSigners() so merchants can decide.
  • EOA vs. contract captureAuthorizer auto-detection — the facilitator probes getCode(captureAuthorizer). Empty bytecode → EOA path, direct call to escrow with msg.sender == captureAuthorizer. Non-empty → contract path, call routed through the contract.
  • assetTransferMethod is per-request — defaults to "eip3009"; set to "permit2" for tokens that lack ERC-3009 (e.g., BSC's Binance-Peg USDC, some L2 USD stablecoins). The server does not auto-pick based on chain; the merchant declares the method explicitly.

Spec observations from implementation

Small clarifications a follow-up doc pass could pick up:

  1. Verify step 13 / Settlement step 5. captureAuthorizer can be the facilitator EOA (call goes to escrow directly) or a passthrough contract (call goes to the contract, which forwards to escrow). The spec's "call AUTH_CAPTURE_ESCROW.authorize/charge" wording is technically correct for both since the contract path reaches escrow underneath, but a revert in the contract case can originate in the passthrough before reaching escrow.

    Relatedly, invalid_capture_authorizer (escrow's InvalidSender) only fires when onlySender(operator) fails — i.e., when the address that called escrow (the facilitator EOA or passthrough contract) doesn't match operator (= extra.captureAuthorizer from PaymentInfo).

    Spelling these out in the verification/settlement logic steps would make the contract path easier to reason about.

  2. Scheme names are inconsistent. Existing schemes are lowercase or kebab-case (exact, upto, batch-settlement); authCapture is camelCase. This breaks tooling that assumes one case.

    The e2e CLI --schemes filter, for instance, lowercases user input and silently dropped all authCapture matches — the case-insensitive compare added in this PR (e2e/src/cli/filters.ts) works around it.

    Picking one convention and applying it backwards (e.g., renaming authCapture to auth-capture if kebab-case wins) would make scheme identifiers uniform.

  3. Reference links are dead. scheme_authCapture.md lines 84-85 link to issues on the legacy github.com/coinbase/x402 repo that no longer resolve; canonical repo is github.com/x402-foundation/x402.

    The org segment should be swapped to x402-foundation to fix them.

What's included

  • @x402/evm/authCapture/{client,server,facilitator} — new subdirectory under the existing @x402/evm package, alongside exact/, upto/, and batch-settlement/. Three new subpath exports added to tsup.config.ts and package.json.
  • Unit tests — 5 files under test/unit/authCapture/, covering client payload construction, server parsePrice / enhancePaymentRequirements, facilitator verify + settle paths for both EIP-3009 and Permit2, nonce / salt derivation, type guards, and the EOA vs. contract captureAuthorizer routing. All 735 package tests pass.
  • Integration testtest/integrations/authCapture-evm.test.ts, Base Sepolia live-testnet style matching the batch-settlement-evm.test.ts pattern. Skips with describe.skip unless CLIENT_PRIVATE_KEY and FACILITATOR_PRIVATE_KEY are set. Covers ERC-3009 + Permit2 × autoCapture: false / true.
  • Examples under examples/typescript/{clients,servers,facilitator}/authCapture/ — three standalone apps mirroring the batch-settlement example layout.
  • E2E harness wiring — scheme registered on the proxy, express, fastify, hono servers and on the TypeScript facilitator. Route files added under e2e/servers/next/app/api/authCapture/evm/{eip3009,permit2}/withx402/. Endpoint entries added to test configs. PaymentScheme type extended in e2e/src/types.ts and PaymentSchemeKind in e2e/src/cli/filters.ts. EVM_AUTHCAPTURE_CAPTURE_AUTHORIZER declared in optional env vars, with conditional registration: when unset, authCapture routes are skipped (matching the EVM_RECEIVER_AUTHORIZER_PRIVATE_KEY pattern used by batch-settlement). AuthCaptureEvmScheme also registered on the fetch and axios e2e clients alongside the existing schemes.
  • NOTICE — paragraph added crediting the x402r team (proposal issue [Proposal] Escrow Scheme for x402 using Base Commerce Payments Protocol #1011, spec PR Add authCapture scheme specification #1425) and propagating the MIT attribution for the Agentokratia "x402-escrow" proposal (issue [Proposal] escrow scheme using Base Commerce Payments Protocol #834) whose code the implementation incorporates.
  • Changeset fragment@x402/evm: minor, "Implemented authCapture mechanism".

Disclosure

This PR was written with AI assistance (Claude Code). Output was reviewed before submission per the AI guidance in CONTRIBUTING.md.

Links

Spec: #1425
Original proposals: #1011, #834

Tests

  • Unit tests (735/735 passing)
  • Integration tests (4/4 passing on Base Sepolia, ERC-3009 + Permit2 × autoCapture: false / true)
  • Examples flow exercised end-to-end on Base Sepolia (facilitator + server + client; real settle tx confirmed)
  • E2E harness (4 server frameworks × 2 transfer methods = 8 scenarios, 8/8 passing on Base Sepolia for express / fastify / hono with fetch and axios clients; next server skipped due to a pre-existing Next 16 + Aptos SDK transitive build issue affecting all schemes, not specific to this PR — not surfaced by CI since .github/workflows/ only runs unit tests, no e2e workflow exists upstream)

Checklist

  • I have formatted and linted my code
  • All new and existing tests pass
  • My commits are signed (required for merge)
  • I added a changelog fragment for user-facing changes

Implements the authCapture scheme per spec x402-foundation#1425. Builds on the
audited base/commerce-payments contract stack, so no new contracts
ship with this PR.

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

vercel Bot commented May 14, 2026

@A1igator is attempting to deploy a commit to the Coinbase Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added typescript sdk Changes to core v2 packages examples Changes to examples labels May 14, 2026
Required by the Check Paywall Template workflow on this PR (the
first to touch `typescript/packages/mechanisms/evm/` since
`dd6d7e6f`). The committed templates had drifted from what a fresh
build produces: the diff is in esbuild's minified identifier names,
not in semantic content.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A1igator added a commit to BackTrackCo/x402r-scheme that referenced this pull request May 15, 2026
## Summary

Two facilitator bugs found while running the same `authCapture` code as
the upstream x402 TypeScript implementation against live Base Sepolia
([x402-foundation/x402#2308](x402-foundation/x402#2308)).
Both reverts only surface against the real escrow / Permit2 collectors
and pure unit-test mocking misses them, which is why they sat
undetected.

## Bug 1: `simulateSettle` missing `account`

`simulateSettle` called `readContract` without `account`, so the
underlying `eth_call` ran with `msg.sender = 0x0`. Escrow's
`onlySender(paymentInfo.operator)` then reverted with `InvalidSender`,
which we map to `invalid_capture_authorizer`. Every
EOA-`captureAuthorizer` verify failed even when the real submitter would
have matched.

Fix: pass `account: this.signer.getAddresses()[0]` so escrow's
`onlySender` is evaluated against the same `msg.sender` the real settle
tx will have (EOA path: facilitator EOA; contract path:
captureAuthorizer contract, which forwards as itself).

The `FacilitatorEvmSigner` type in `@x402/evm` doesn't yet declare
`account` on `readContract` — viem's underlying `readContract` accepts
it. Carrying the value through with a local `Parameters<...>` cast; the
upstream signer-type update is pending in
[x402-foundation/x402#2308](x402-foundation/x402#2308)
and once merged we can drop the cast.

## Bug 2: Permit2 `collectorData` was ABI-wrapped

`unpackForSettle` wrapped the Permit2 signature with
`encodeAbiParameters([{ type: "bytes" }], [sig])`. Permit2's collector
validates `signature.length == 65` on the raw byte array and reverted
with `InvalidSignatureLength()` for every Permit2 settle.

Fix: pass `p.signature` directly. Matches the EIP-3009 path, which never
wrapped.

## How they were missed

Unit tests mock `readContract` and never exercise the on-chain side, so
neither the `account`-on-simulate gap nor the Permit2 length check
fired. They only surface in an end-to-end run against the real escrow +
collector contracts. Adding an integration test against the canonical
AuthCaptureEscrow on Base Sepolia (the same pattern upstream's
`batch-settlement-evm.test.ts` follows) would catch them — worth a
follow-up.

## Test plan

- [x] `pnpm test` — 124/124 unit tests pass
- [x] `pnpm lint:check` — clean
- [x] `pnpm format:check` — clean
- [x] Fixes validated end-to-end on Base Sepolia inside the upstream PR
([#2308](x402-foundation/x402#2308)): 4/4
integration test cases pass (ERC-3009 / Permit2 × autoCapture: false /
true), 8/8 e2e harness scenarios pass across express / fastify / hono
with fetch and axios clients. Real settle txs confirmed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread typescript/packages/mechanisms/evm/src/authCapture/server/scheme.ts Outdated
Comment thread typescript/packages/mechanisms/evm/src/authCapture/server/scheme.ts Outdated
Comment thread examples/typescript/servers/authCapture/index.ts Outdated
Comment on lines +30 to +35
// Static deadlines: every authorization created by this server expires at
// the same absolute Unix second. Production servers typically compute these
// per request via custom middleware; static deadlines suffice for the demo.
const now = Math.floor(Date.now() / 1000);
const captureDeadline = now + 30 * 86400;
const refundDeadline = now + 60 * 86400;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this should be done in enhancePaymentRequirements, so its fresh for each request

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added *Seconds variants of them that are filled by enhancePaymentRequirements. Let me know if that was not the intention.

* @param _ - Unused FacilitatorContext (interface compatibility).
* @returns A `VerifyResponse` with `isValid` and, on failure, a stable `invalidReason`.
*/
async verify(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

For the permit2 branch we should add support for the ERC-20 approval gas sponsoring extension

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is there a base commerce deposit collector that supports EIP-2612?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't believe so. I can build one and include here/contribute to base commerce-payments repo but not sure how audits and stuff work for that.

@phdargen phdargen self-assigned this May 15, 2026
@phdargen
Copy link
Copy Markdown
Collaborator

Thanks a lot for the contribution @A1igator, made a first pass and looks good! Please have a look at my comments above

A1igator and others added 3 commits May 17, 2026 20:44
The paywall template changes in this PR were a CI workaround. The
authCapture work doesn't touch paywall logic, but CI was failing on a
paywall consistency check unrelated to this PR (template drift on main
at the time). Regenerating the templates locally made CI green.

Main has since moved on with its own paywall fixes, so the stale regen
is now both unnecessary and the source of a merge conflict against
upstream/main. Revert the 9 template files to the merge-base state so
the merge from upstream/main applies cleanly.

If CI fails on a paywall check again after this revert, the previous
regen was masking a real issue and will be addressed in a separate PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address phdargen's four review comments on this PR plus a related
hardening that emerged in the back-and-forth.

- Drop local ASSET_INFO in favor of getDefaultAsset from
  ../../shared/defaultAssets. Merchants on chains not in
  DEFAULT_STABLECOINS (Ethereum mainnet, Optimism, Celo, Avalanche,
  Linea, Sepolia, BSC, Tempo) now pass an explicit AssetAmount instead
  of using the decimal fallback, matching how exact and upto handle the
  same situation.
- Drop local string-math convertToTokenAmount in favor of the shared
  @x402/core/utils helpers. Same algorithm, one source of truth.
- Drop the manually-set name/version from the example server's
  accepts.extra; parsePrice resolves them from the default-asset table.
- Move boot-time deadline computation into enhancePaymentRequirements
  so each request gets a fresh window. Merchants set per-route relative
  offsets in extra.captureDeadlineSeconds / extra.refundDeadlineSeconds
  (matching the *Seconds suffix from maxTimeoutSeconds); the scheme
  converts to absolute captureDeadline / refundDeadline per request and
  strips the offset keys. Absolute deadlines are still accepted for
  externally-driven cases (e.g., tied to a delivery commit); absolute
  wins over relative, per-field mix supported.

Per-route windows let a high-cost long-running job carry a different
envelope from a cheap one-shot call; the scheme has no constructor
configuration for deadlines, which would have forced one global policy
onto every route using the scheme instance.

Asymmetric fail-fast (matches batch-settlement's pattern):
enhancePaymentRequirements throws when a merchant-set required field
(captureAuthorizer, deadlines, feeRecipient, min/maxFeeBps) is missing
or wrongly-typed, so misconfiguration lands in merchant logs instead of
in 402s to payers. name/version are deliberately excluded because
parsePrice auto-populates them; merchants supplying a custom AssetAmount
who forget them still get caught by the facilitator's isAuthCaptureExtra
guard.

E2E wiring (express, fastify, hono, next-proxy + two next route files)
updated to the *Seconds shape and dropped hardcoded name/version.

754/754 tests pass (+17 over baseline).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

examples Changes to examples sdk Changes to core v2 packages typescript

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants