Skip to content

feat: add unified EVM session payment method#225

Open
clayclaw wants to merge 14 commits into
tempoxyz:mainfrom
okx:add-evm-session
Open

feat: add unified EVM session payment method#225
clayclaw wants to merge 14 commits into
tempoxyz:mainfrom
okx:add-evm-session

Conversation

@clayclaw

@clayclaw clayclaw commented Apr 6, 2026

Copy link
Copy Markdown

Summary

Adds the session intent for the evm payment method, defining unidirectional streaming payment channels for incremental, voucher-based payments on any EVM-compatible chain. Adapts the streaming payment channel mechanism from the Tempo session spec (draft-tempo-session) for general EVM use, with EVM-specific transaction formats, gas models, and domain separators.

  • Request schema: amount (price per unit, base units), currency (ERC-20 address), recipient (payee address), with methodDetails for chainId, escrowContract, feePayer, optional splits
  • Escrow contract: 8 on-chain functions covering channel open, settle, topUp, close, and forced close paths
  • Credential: four payload actions (open, topUp, voucher, close) with EIP-712 voucher signatures for off-chain cumulative payments
  • Verification: per-action checks — on-chain state for open/topUp, signature recovery + monotonicity for vouchers, final settlement for close
  • Receipt: per-request Payment-Receipt with acceptedCumulative, spent, optional reference (tx hash on settlement)

This is an initial draft, we are actively developing the underlying functionality and will continue refining the spec as implementation progresses. Feedback and discussion are welcome.

Context

This is the second intent defined for the evm payment method (after charge in #213). The session intent follows the Tempo session spec (draft-tempo-session) as the reference design, extending it to work across any EVM-compatible chain.

Design decisions

Decision Rationale
On-chain escrow contract with 8 functions open, openWithAuthorization, settle, topUp, topUpWithAuthorization, close, requestClose, withdraw — covers the full channel lifecycle with both client-funded and server-submitted paths.
openWithAuthorization / topUpWithAuthorization via EIP-3009 When feePayer: true, client signs off-chain transferWithAuthorization; server calls the escrow contract and pays gas. Client never needs native token. Requires EIP-3009 token support.
open() + ERC-4337 support for client-broadcast path When feePayer: false, client calls approve() + open() directly or via an EntryPoint-mediated UserOperation. Smart wallets can batch both in a single UserOp.
Deterministic channelId via keccak256(payer, payee, token, salt, authorizedSigner, escrow, chainId) Identical to Tempo's formula. Binds channel to a specific contract deployment and chain. Client can pre-compute before the on-chain tx lands.
Deposit merge in voucher payload Bundles an EIP-3009 topUp (or initial open) with a voucher update in a single HTTP round-trip — reduces latency when approaching deposit limits.
Splits as immutable on-chain splitRecipients[] + splitBps[] Session total is unknown upfront, so basis points (not fixed amounts). Enforced atomically by the contract at settle() / close().
Domain separator: "EVM Payment Channel" Distinguishes from Tempo's "Tempo Stream Channel" — the only semantic difference in the voucher signing scheme. Prevents cross-deployment replay.

New File

  • specs/methods/evm/draft-evm-session-00.md

AI Disclosure

This spec was drafted with AI assistance (Claude). All content has been reviewed for technical accuracy, RFC compliance, and alignment with STYLE.md / CONTRIBUTING.md.

michael.wong and others added 6 commits April 7, 2026 03:12
  - Switch escrow from transferWithAuthorization to receiveWithAuthorization (enforces msg.sender == to, preventing mempool signature extraction attacks per EIP-3009 security guidance)
  - Add security consideration for parameter substitution attack: EIP-3009 signature does not cover channel params (payee, salt, etc), so an attacker can front-run with modified params. Recommend deriving nonce deterministically from
   channel parameters as mitigation
  - Fix bibliographic references: add missing co-authors for EIP-3009, EIP-2098, EIP-712; correct EIP-3009 date to 2020-09
  - Replace (v, r, s) params with packed bytes signature for interface consistency with settle/close
- topUpWithAuthorization: rename salt->topUpSalt, add from to nonce derivation (keccak256(channelId, additionalDeposit, from, topUpSalt))

-
  EIP-2098: keep MUST NOT produce, relax to MAY accept compact signatures

- authorizedSigner: allow ERC-1271 contract wallets (Safe/4337), not EOA-only

- close: document voucher-less forfeit path when cumulativeAmount <=
  settled

- EIP-3009: clarify escrow targets USDC v2.2+ bytes overload only; canonical (v,r,s) tokens not supported

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Comment thread specs/methods/evm/draft-evm-session-00.md
Comment thread specs/methods/evm/draft-evm-session-00.md Outdated
Comment thread specs/methods/evm/draft-evm-session-00.md Outdated
Comment thread specs/methods/evm/draft-evm-session-00.md Outdated
Comment thread specs/methods/evm/draft-evm-session-00.md
Comment thread specs/methods/evm/draft-evm-session-00.md
Comment thread specs/methods/evm/draft-evm-session-00.md
@brendanjryan

Copy link
Copy Markdown
Collaborator

Directionally looks good -- thanks for putting this together!

I think one thing to consider is support for non-EIP3009 tokens via permit or similar.

This isn't strictly required at this time, but we should make sure the design will scale to this model without breaking the core interface

Switch openWithPermit2 and topUpWithPermit2 to permitWitnessTransferFrom, carrying channel parameters (payee, salt,
  authorizedSigner for open; channelId, topUpSalt for topUp) as a named EIP-712 witness struct. Wallets that render typed data now display these as labeled fields at signing time instead of having them hashed
  opaquely into a derived nonce.

EIP-3009 has no witness mechanism, so it keeps the deterministic-nonce derivation for channel-parameter binding.
@brendanjryan

brendanjryan commented May 11, 2026

Copy link
Copy Markdown
Collaborator

Two issues that should be fixed before this lands:

  1. Stale voucher handling currently returns 200 OK for cumulativeAmount <= highestVoucherAmount before verifying the voucher signature/signer.

Since voucher credentials can omit source and identify the payer from channel state, anyone who knows a channelId could submit a stale or low unsigned voucher and potentially consume already-authorized balance. Please verify signer first, or only treat an exact stored credential/signature replay as idempotent.

  1. The EIP-3009 channel-parameter binding needs to be mandatory.

The draft correctly notes that EIP-3009 does not bind payee, salt, or authorizedSigner, but deterministic nonce derivation is only a SHOULD. Because openWithAuthorization is callable by anyone, a relayer that sees the authorization could front-run with attacker-controlled channel parameters. Please make the deterministic nonce derivation a MUST and require the escrow to reject a supplied nonce that does not match the derived value, or remove the nonce argument and derive it internally.

@github-actions

github-actions Bot commented May 11, 2026

Copy link
Copy Markdown

Spec Preview

Spec Changed Artifacts
draft-card-charge-00 Yes HTML · TXT · XML · PDF
draft-evm-charge-00 Yes HTML · TXT · XML · PDF
draft-evm-session-00 New HTML · TXT · XML · PDF
draft-hedera-charge-00 Yes HTML · TXT · XML · PDF
draft-httpauth-payment-00 Yes HTML · TXT · XML · PDF
draft-lightning-charge-00 Yes HTML · TXT · XML · PDF
draft-lightning-session-00 Yes HTML · TXT · XML · PDF
draft-payment-discovery-00 Yes HTML · TXT · XML · PDF
draft-payment-intent-charge-00 Yes HTML · TXT · XML · PDF
draft-payment-transport-mcp-00 Yes HTML · TXT · XML · PDF
draft-solana-charge-00 Yes HTML · TXT · XML · PDF
draft-stellar-charge-00 Yes HTML · TXT · XML · PDF
draft-stripe-charge-00 Yes HTML · TXT · XML · PDF
draft-tempo-charge-00 Yes HTML · TXT · XML · PDF
draft-tempo-session-00 Yes HTML · TXT · XML · PDF

Browse preview release assets

@brendanjryan

Copy link
Copy Markdown
Collaborator

cc @clayclaw -- let me know when this is ready fro another review!

@chopmob-cloud

Copy link
Copy Markdown

+1 on the unified EVM approach. We ship the x402 v2 canonical envelope identically across Base and Tempo (USDC on both) with zero per-chain branching at the HTTP layer, so the design intent of "one session spec, multiple EVM chains" matches what works in production.

A few operational notes from running both chains live (AlgoVoi gateway):

  • Revert short-circuit: eth_getTransactionReceipt status=0x0 should be handled as a typed error rather than waiting for the spinner timeout. Worth calling out as a verifier obligation so session-close paths don't hang on a reverted settlement.
  • Deterministic channelId binding: including chainId in the keccak formula prevents the cross-deployment replay you'd otherwise hit if a payer reused the same authorized-signer across two EVMs. We use the same pattern on Recurr Tier 2 EVM authorities and it simplifies replay reasoning significantly.
  • Fee-absorption tolerance on voucher monotonicity: gas-price spikes on Base can cause topUp to land for less than the client expected after slippage. Our CCTP V2 xchain flow uses a tolerated_min_delivered field to absorb the 14 bps Fast Transfer fee. An analogous tolerance on the voucher cumulativeAmount boundary might be worth specifying so channels don't force-close on a thin margin.
  • On @brendanjryan's EIP-3009 vs permit concern: this matches our experience that USDC's EIP-3009 coverage is the happy path; for non-3009 tokens our Recurr Tier 2 EVM module falls back to approve() + transfer with the user-broadcast path, which is the same shape as the feePayer: false branch here. The interface should scale to permit-based and approve-based tokens without breaking the core abstraction.

Happy to dig into any of these if useful for the spec text.

Co-authored-by: Cursor <cursoragent@cursor.com>
@chopmob-cloud

Copy link
Copy Markdown

Coming at this from the non-EVM side of the same problem space — AlgoVoi ships AP2 (Agent Payment Protocol) mandates as the Algorand/VOI equivalent of the session intent defined here, so some of the design decisions look familiar. Happy to contribute production observations as this develops.

Two points that may be useful as the spec is refined:

On EIP-3009 universality. brendanjryan's point is well-placed. On Algorand/VOI we hit the same gap: not all assets support the off-chain-authorisation pattern, so we run a fallback path where the client pre-funds an escrow and the server draws down against it without needing per-call authorisation. Permit2 is the right EVM analogue — worth specifying the fallback path explicitly in the spec rather than leaving it to implementers, otherwise you get fragmentation between EIP-3009-capable and non-capable token deployments.

On splits at settlement. splitBps[] enforced at settle() / close() is the right shape — we run the same pattern in production. One edge case worth covering: what happens if a split recipient is a contract that reverts on receive? A push-based split at settlement can deadlock the channel close. A pull-based alternative (split recipients claim their share post-settlement) avoids the revert-griefing vector entirely.

Both are refinement points rather than blockers. The domain separator distinction from Tempo's "Tempo Stream Channel" is correct practice. We're following this thread and glad to help stress-test the spec further as implementation progresses — cross-chain production experience with the same intent pattern is a useful reference point.

AlgoVoi (chopmob-cloud) — Acquisition enquiries: https://docs.algovoi.co.uk/acquisition

@clayclaw

clayclaw commented Jun 2, 2026

Copy link
Copy Markdown
Author

@brendanjryan Thanks, both issues are addressed in the latest draft. Voucher idempotency now runs only after EIP-712 signature and signer verification, and the EIP-3009 channel-parameter binding is now mandatory.

@chopmob-cloud Thanks for the production notes, we added explicit receipt.status == 0x0 handling with a typed transaction-reverted error so settle/close don’t hang on reverts. On voucher cumulative-amount tolerance for gas/top-up slippage, we’re leaning toward keeping that chain-specific rather than normative here, and we’re open to community discussion if others want a normative hook.

In this revision we also split the escrow into a mandatory core and an optional relayed / gasless operations profile for relayer-submitted paths (EIP-3009, Permit2, payee-authorized settle/close), with advertised capabilities aligned to what the escrow actually implements.

Settlement amounts must strictly increase: the escrow reverts stale vouchers on-chain, and servers apply the same rule off-chain. Finalized channels stay on record permanently, with a cross-epoch replay section, so reusing the same channelId cannot revive old vouchers against a new deposit.

@clayclaw

clayclaw commented Jun 2, 2026

Copy link
Copy Markdown
Author

Coming at this from the non-EVM side of the same problem space — AlgoVoi ships AP2 (Agent Payment Protocol) mandates as the Algorand/VOI equivalent of the session intent defined here, so some of the design decisions look familiar. Happy to contribute production observations as this develops.

Two points that may be useful as the spec is refined:

On EIP-3009 universality. brendanjryan's point is well-placed. On Algorand/VOI we hit the same gap: not all assets support the off-chain-authorisation pattern, so we run a fallback path where the client pre-funds an escrow and the server draws down against it without needing per-call authorisation. Permit2 is the right EVM analogue — worth specifying the fallback path explicitly in the spec rather than leaving it to implementers, otherwise you get fragmentation between EIP-3009-capable and non-capable token deployments.

On splits at settlement. splitBps[] enforced at settle() / close() is the right shape — we run the same pattern in production. One edge case worth covering: what happens if a split recipient is a contract that reverts on receive? A push-based split at settlement can deadlock the channel close. A pull-based alternative (split recipients claim their share post-settlement) avoids the revert-griefing vector entirely.

Both are refinement points rather than blockers. The domain separator distinction from Tempo's "Tempo Stream Channel" is correct practice. We're following this thread and glad to help stress-test the spec further as implementation progresses — cross-chain production experience with the same intent pattern is a useful reference point.

AlgoVoi (chopmob-cloud) — Acquisition enquiries: https://docs.algovoi.co.uk/acquisition

Thanks for the AP2 context. We just added non–EIP-3009 funding path with Permit2 in the relayed profile, with feePayerAuthorizations for discovery. We had split / splitBps in an earlier draft but removed them from the unified escrow surface: revenue splits feel better as an implementor extension than core session semantics, especially with extra gas at settle/close that varies by chain. This also adds the settlement complexity. Your push-vs-pull revert point is still a useful note for anyone layering splits on top. Thanks for sharing these notes.

@chopmob-cloud

Copy link
Copy Markdown

The channel mechanics are well-structured — the deterministic channelId, EIP-712 voucher monotonicity, and the openWithAuthorization / topUpWithAuthorization split across fee-payer paths cover the core EVM constraints cleanly.

One gap worth addressing before the spec stabilises: the settlement receipt on close / settle is currently thin — acceptedCumulative, spent, and an optional reference (tx hash). This doesn't distinguish terminal states as positive records. A verifier looking at a channel history cannot tell the difference between a clean close (mutual agreement), a requestClose timeout path, and a forced withdraw — they all produce the same receipt shape, or no receipt at all if the tx hash isn't present.

The pattern that closes this: a terminal state enum (SETTLED / PENDING_FINALITY / REVERSED) carried in the settlement receipt alongside a content-addressed settled_payment_ref and a canon_version pin. The verifier recomputes the receipt hash from canonical bytes and can distinguish final settlement from forced close from reversal as byte-distinct records — without querying the escrow contract state at audit time.

This is specified in draft-hopley-x402-settlement-attestation and runs in production across AlgoVoi's EVM payment flows today. The reference implementation is at @algovoi/settlement-attestation (Apache 2.0). Happy to cross-reference the spec if it's useful for the settlement receipt shape here.

AlgoVoi (chopmob-cloud) — docs.algovoi.co.uk/settlement-attestation

@brendanjryan brendanjryan left a comment

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.

Inline review of two cross-spec consistency issues with the other evm/session specs on paymentauth.org (see comments below).

Comment thread specs/methods/evm/draft-evm-session-00.md Outdated
Comment thread specs/methods/evm/draft-evm-session-00.md Outdated
@brendanjryan

Copy link
Copy Markdown
Collaborator

@clayclaw -- left a few more comments, but this looks pretty close!

Along with #201, we are going to try to formalize the generic "session" intent this week, which should unblock this pr from merging.

@brendanjryan

Copy link
Copy Markdown
Collaborator

#280

Here is the generic session spec, can you look at the above and flag any deltas or misalignment?

@clayclaw

Copy link
Copy Markdown
Author

#280

Here is the generic session spec, can you look at the above and flag any deltas or misalignment?

Thanks. Let me take a look

clayclaw added 4 commits June 15, 2026 11:33
Reuse the shared EVM top-level credential type names and replace the separate fee-payer authorization negotiation with methodDetails.credentialTypes, matching the charge intent semantics.
Update the EVM session draft to reference the generic session intent registration, avoid duplicate IANA intent registration, and align receipt semantics so reference remains the stable channel identifier while settlement transaction hashes are reported separately.
@clayclaw

Copy link
Copy Markdown
Author

#280

Here is the generic session spec, can you look at the above and flag any deltas or misalignment?

We reviewed the generic session intent PR and aligned the EVM session draft where it makes sense.

Apart from the IANA cleanup, the main alignment change was to receipt semantics.

Change:

  • Aligned receipt semantics: reference is now the stable session reference and equals channelId; on-chain transaction hashes are returned separately as optional txHash on settlement/close receipts.

One note: we did not change EVM’s insufficient-balance problem type to the generic draft’s insufficient-value. Tempo already uses https://paymentauth.org/problems/session/insufficient-balance, EVM matches that, and the currently working URI appears to be the balance one. Can you confirm if this is a possible inconsistency in the generic draft rather than something method specs should all change?

@clayclaw

Copy link
Copy Markdown
Author

The channel mechanics are well-structured — the deterministic channelId, EIP-712 voucher monotonicity, and the openWithAuthorization / topUpWithAuthorization split across fee-payer paths cover the core EVM constraints cleanly.

One gap worth addressing before the spec stabilises: the settlement receipt on close / settle is currently thin — acceptedCumulative, spent, and an optional reference (tx hash). This doesn't distinguish terminal states as positive records. A verifier looking at a channel history cannot tell the difference between a clean close (mutual agreement), a requestClose timeout path, and a forced withdraw — they all produce the same receipt shape, or no receipt at all if the tx hash isn't present.

The pattern that closes this: a terminal state enum (SETTLED / PENDING_FINALITY / REVERSED) carried in the settlement receipt alongside a content-addressed settled_payment_ref and a canon_version pin. The verifier recomputes the receipt hash from canonical bytes and can distinguish final settlement from forced close from reversal as byte-distinct records — without querying the escrow contract state at audit time.

This is specified in draft-hopley-x402-settlement-attestation and runs in production across AlgoVoi's EVM payment flows today. The reference implementation is at @algovoi/settlement-attestation (Apache 2.0). Happy to cross-reference the spec if it's useful for the settlement receipt shape here.

AlgoVoi (chopmob-cloud) — docs.algovoi.co.uk/settlement-attestation

Thanks for the pointer. The session drafts seem to use a lightweight receipt model today: accounting state plus method-native evidence like txHash.

The proposed attestation format looks broader than this EVM session draft, and adding it only here may make EVM diverge from the other session methods. This may fit better as a shared receipt extension across session methods later.

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.

3 participants