Permit2 PermitWitnessTransferFrom building, EIP-712 signing, and proxy
settlement calldata for the x402 exact (Permit2 transfer method) and
upto schemes on EVM networks.
Implements the client half of both Permit2-based payment flows: building a
Permit2 authorization from v2 payment requirements, computing its EIP-712
digest against the canonical Permit2 domain, and signing it through the
X402.Signer behaviour to produce the scheme payload map
%{"signature" => "0x...", "permit2Authorization" => %{...}}carried inside a v2 PaymentPayload (see X402.Client.build_payment/3
for the full envelope). It also ABI-encodes the settle calls the
facilitator broadcasts to the x402 proxy contracts
(exact_settle_calldata/2, upto_settle_calldata/3), shared by
X402.Verify.EVM's simulation and X402.Facilitator.Engine's settlement.
The authorizations
In both flows the client signs a PermitWitnessTransferFrom message for
the canonical Permit2 contract
(0x000000000022D473030F116dDEE9F6B43aC78BA3) where
permitted.token / permitted.amount are the requirements' asset and
amount, and the spender is an x402 proxy contract deployed at the
same address on every supported EVM chain via CREATE2. The proxy is the
only party that can consume the permit, and it enforces the witness:
- exact (
extra.assetTransferMethod"permit2") — the spender is thex402ExactPermit2Proxy(0x402085c248EeA27D92E8b30b2C58ed07f9E20001) and the witnessWitness(address to,uint256 validAfter)binds the requirements'payTo. The permitted amount is the exact amount settled. - upto — the spender is the
x402UptoPermit2Proxy(0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002) and the witnessWitness(address to,address facilitator,uint256 validAfter)bindspayToand the facilitator announced inextra.facilitatorAddress, so no other party can settle. The permitted amount is a maximum; the facilitator settles the actual usage up to that ceiling.
The witness shape selects the typed-data variant: an authorization whose
witness carries a facilitator field hashes as the upto type, otherwise
as the exact type.
The EIP-712 domain is the canonical Permit2 domain — name "Permit2",
the chain id from the CAIP-2 network, the Permit2 contract as the
verifying contract, and no version field.
Cryptographic operations require the optional ex_secp256k1 and
ex_keccak dependencies and return {:error, :missing_dependency} when
they are unavailable; the library itself compiles without them.
Summary
Payment Settlement
Builds the x402ExactPermit2Proxy.settle calldata for an exact
authorization and its raw (inner) signature bytes.
Builds the x402UptoPermit2Proxy.settle calldata for an upto
authorization, the amount to settle, and the raw (inner) signature bytes.
Types
A Permit2 PermitWitnessTransferFrom authorization in wire shape.
The scheme payload map for a signed Permit2 payment.
Functions
Builds an exact PermitWitnessTransferFrom authorization in wire shape.
Builds an upto PermitWitnessTransferFrom authorization in wire shape.
Computes the EIP-712 digest of a PermitWitnessTransferFrom.
Derives the canonical Permit2 EIP-712 domain from v2 payment requirements.
Returns the x402ExactPermit2Proxy contract address — the spender of
exact payments using the Permit2 transfer method.
Fetches the facilitator address from the requirements' extra.
Returns the canonical Permit2 contract address.
Returns a fresh random uint256 nonce as a decimal string.
Signs an authorization's EIP-712 digest with a signer.
Signs the exact Permit2 scheme payload for the given requirements.
Signs the upto Permit2 scheme payload for the given requirements.
Computes the EIP-712 digest of a PermitWitnessTransferFrom — same as
digest/2.
Derives the canonical Permit2 EIP-712 domain — same as domain/1.
Returns the x402UptoPermit2Proxy contract address — the upto spender.
Payment Settlement
@spec exact_settle_calldata(map(), binary()) :: {:ok, binary()} | {:error, encode_error() | :invalid_signature}
Builds the x402ExactPermit2Proxy.settle calldata for an exact
authorization and its raw (inner) signature bytes.
Encodes settle(PermitTransferFrom permit, address owner, Witness witness, bytes signature) (selector 0x13cd3b53) where permit is
((permitted.token, permitted.amount), nonce, deadline), owner is the
authorization's from, and the witness is (witness.to, witness.validAfter). The proxy forwards the call to Permit2's
permitWitnessTransferFrom with permitted.amount as the requested
amount. Shared by X402.Verify.EVM's eth_call simulation and
X402.Facilitator.Engine's settlement transaction.
Examples
iex> asset = "0x2222222222222222222222222222222222222222"
iex> authorization = %{
...> "from" => "0x1111111111111111111111111111111111111111",
...> "permitted" => %{"token" => asset, "amount" => "10000"},
...> "spender" => "0x402085c248EeA27D92E8b30b2C58ed07f9E20001",
...> "nonce" => "7",
...> "deadline" => "99999999999",
...> "witness" => %{"to" => "0x3333333333333333333333333333333333333333", "validAfter" => "0"}
...> }
iex> {:ok, calldata} = X402.Permit2.exact_settle_calldata(authorization, <<1::520>>)
iex> {binary_part(calldata, 0, 4), byte_size(calldata)}
{<<0x13, 0xCD, 0x3B, 0x53>>, 4 + 8 * 32 + 32 + 96}
iex> X402.Permit2.exact_settle_calldata(%{}, <<1::520>>)
{:error, {:missing_field, "permitted"}}
@spec upto_settle_calldata(map(), String.t() | non_neg_integer(), binary()) :: {:ok, binary()} | {:error, encode_error() | :invalid_signature}
Builds the x402UptoPermit2Proxy.settle calldata for an upto
authorization, the amount to settle, and the raw (inner) signature bytes.
Encodes settle(PermitTransferFrom permit, uint256 amount, address owner, Witness witness, bytes signature) (selector 0xff11e7b4) where the
witness is (witness.to, witness.facilitator, witness.validAfter) and
amount is the actual settlement amount — at most permitted.amount,
which the proxy enforces (AmountExceedsPermitted). The proxy also
requires msg.sender to equal witness.facilitator
(UnauthorizedFacilitator).
Examples
iex> asset = "0x2222222222222222222222222222222222222222"
iex> authorization = %{
...> "from" => "0x1111111111111111111111111111111111111111",
...> "permitted" => %{"token" => asset, "amount" => "10000"},
...> "spender" => "0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002",
...> "nonce" => "7",
...> "deadline" => "99999999999",
...> "witness" => %{
...> "to" => "0x3333333333333333333333333333333333333333",
...> "facilitator" => "0x4444444444444444444444444444444444444444",
...> "validAfter" => "0"
...> }
...> }
iex> {:ok, calldata} = X402.Permit2.upto_settle_calldata(authorization, "9000", <<1::520>>)
iex> {binary_part(calldata, 0, 4), byte_size(calldata)}
{<<0xFF, 0x11, 0xE7, 0xB4>>, 4 + 10 * 32 + 32 + 96}
iex> X402.Permit2.upto_settle_calldata(%{}, "1", <<1::520>>)
{:error, {:missing_field, "permitted"}}
Types
A Permit2 PermitWitnessTransferFrom authorization in wire shape.
String keys "from", "permitted" (%{"token", "amount"}),
"spender", "nonce", "deadline", and "witness"
(%{"to", "validAfter"} for exact, %{"to", "facilitator", "validAfter"}
for upto).
@type authorization_error() :: :invalid_requirements | {:missing_extra, String.t()}
@type domain_error() :: :invalid_requirements | :unsupported_network
@type encode_error() :: X402.EIP712.encode_error()
@type payload() :: %{optional(String.t()) => String.t() | authorization()}
The scheme payload map for a signed Permit2 payment.
Functions
@spec build_exact_authorization(map(), String.t()) :: {:ok, authorization()} | {:error, :invalid_requirements}
Builds an exact PermitWitnessTransferFrom authorization in wire shape.
permitted carries the requirements' asset and exact amount; the
spender is the x402ExactPermit2Proxy; the nonce is a fresh random
uint256; the deadline is now plus maxTimeoutSeconds; and the witness
binds payTo (to) with validAfter "0" (immediately valid),
mirroring the reference SDKs.
Field values are validated when the digest is computed, not here.
@spec build_upto_authorization(map(), String.t()) :: {:ok, authorization()} | {:error, authorization_error()}
Builds an upto PermitWitnessTransferFrom authorization in wire shape.
permitted carries the requirements' asset and maximum amount; the
spender is the x402UptoPermit2Proxy; the nonce is a fresh random
uint256; the deadline is now plus maxTimeoutSeconds; and the witness
binds payTo (to), extra.facilitatorAddress (facilitator), and
validAfter "0" (immediately valid), mirroring the reference SDKs.
Field values are validated when the digest is computed, not here.
@spec digest(map(), map()) :: {:ok, <<_::256>>} | {:error, encode_error()}
Computes the EIP-712 digest of a PermitWitnessTransferFrom.
Returns keccak256(0x19 0x01 || domainSeparator || structHash) as a
32-byte binary — the value the client signs and the value to recover
the payer address from (X402.EIP3009.recover_signer/2). The witness
shape selects the type string: a witness with a facilitator field
hashes as the upto Witness(address to,address facilitator,uint256 validAfter), otherwise as the exact Witness(address to,uint256 validAfter). domain and authorization accept both the internal
snake-case atom keys and the wire-style string keys; the authorization's
from is not part of the signed struct (Permit2 recovers the owner from
the signature).
@spec domain(map()) :: {:ok, X402.EIP712.domain()} | {:error, domain_error()}
Derives the canonical Permit2 EIP-712 domain from v2 payment requirements.
The domain is name: "Permit2", the chain id from the CAIP-2 network,
and the canonical Permit2 contract as the verifying contract. Permit2
declares no domain version, so the returned map carries no :version
key and X402.EIP712.domain_separator/1 hashes the three-field
EIP712Domain type. The same domain serves the exact and upto flows.
Examples
iex> X402.Permit2.domain(%{"network" => "eip155:84532"})
{:ok,
%{
name: "Permit2",
chain_id: 84532,
verifying_contract: "0x000000000022D473030F116dDEE9F6B43aC78BA3"
}}
iex> X402.Permit2.domain(%{"network" => "solana:mainnet"})
{:error, :unsupported_network}
@spec exact_proxy_address() :: String.t()
Returns the x402ExactPermit2Proxy contract address — the spender of
exact payments using the Permit2 transfer method.
Deployed at the same address on every supported EVM chain via CREATE2.
Examples
iex> X402.Permit2.exact_proxy_address()
"0x402085c248EeA27D92E8b30b2C58ed07f9E20001"
Fetches the facilitator address from the requirements' extra.
The upto scheme requires extra.facilitatorAddress — the facilitator
announces it via GET /supported (X402.Facilitator.supported/1) and
the resource server forwards it in each upto requirements entry; the
client binds it into the signed witness.
Examples
iex> X402.Permit2.facilitator_address(%{
...> "extra" => %{"facilitatorAddress" => "0x2222222222222222222222222222222222222222"}
...> })
{:ok, "0x2222222222222222222222222222222222222222"}
iex> X402.Permit2.facilitator_address(%{"extra" => %{}})
{:error, {:missing_extra, "facilitatorAddress"}}
@spec permit2_address() :: String.t()
Returns the canonical Permit2 contract address.
Examples
iex> X402.Permit2.permit2_address()
"0x000000000022D473030F116dDEE9F6B43aC78BA3"
@spec random_nonce() :: String.t()
Returns a fresh random uint256 nonce as a decimal string.
Permit2 uses unordered nonces; the reference SDKs draw 32 random bytes per authorization, making collisions negligible.
Examples
iex> nonce = X402.Permit2.random_nonce()
iex> String.match?(nonce, ~r/^[0-9]+$/)
true
@spec sign_authorization(X402.Signer.t(), map(), map()) :: {:ok, String.t()} | {:error, encode_error() | term()}
Signs an authorization's EIP-712 digest with a signer.
The witness shape selects the exact or upto typed data (see the module
documentation). Returns the 0x-prefixed 65-byte r || s || v
signature.
@spec sign_exact(map(), X402.Signer.t()) :: {:ok, payload()} | {:error, domain_error() | authorization_error() | encode_error() | term()}
Signs the exact Permit2 scheme payload for the given requirements.
Derives the canonical Permit2 domain from the requirements' network,
builds a fresh authorization from the signer's address (from), the
requirements' asset/amount (permitted), payTo (the witness), and
maxTimeoutSeconds (the deadline), computes the EIP-712 digest, and
signs it through signer.
Examples
{:ok, signer} = X402.Signer.LocalKey.new(private_key)
{:ok, %{"signature" => _, "permit2Authorization" => _}} =
X402.Permit2.sign_exact(
%{
"scheme" => "exact",
"network" => "eip155:84532",
"amount" => "10000",
"asset" => "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"payTo" => "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
"maxTimeoutSeconds" => 60,
"extra" => %{"assetTransferMethod" => "permit2", "name" => "USDC", "version" => "2"}
},
signer
)
@spec sign_upto(map(), X402.Signer.t()) :: {:ok, payload()} | {:error, domain_error() | authorization_error() | encode_error() | term()}
Signs the upto Permit2 scheme payload for the given requirements.
Derives the canonical Permit2 domain from the requirements' network,
builds a fresh authorization from the signer's address (from), the
requirements' asset/amount (permitted), payTo and
extra.facilitatorAddress (the witness), and maxTimeoutSeconds (the
deadline), computes the EIP-712 digest, and signs it through signer.
Requirements without extra.facilitatorAddress return
{:error, {:missing_extra, "facilitatorAddress"}} — the facilitator
address is required so the witness can bind settlement to it.
Examples
{:ok, signer} = X402.Signer.LocalKey.new(private_key)
{:ok, %{"signature" => _, "permit2Authorization" => _}} =
X402.Permit2.sign_upto(
%{
"scheme" => "upto",
"network" => "eip155:84532",
"amount" => "5000000",
"asset" => "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"payTo" => "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
"maxTimeoutSeconds" => 300,
"extra" => %{
"name" => "USDC",
"version" => "2",
"facilitatorAddress" => "0x2222222222222222222222222222222222222222"
}
},
signer
)
@spec upto_digest(map(), map()) :: {:ok, <<_::256>>} | {:error, encode_error()}
Computes the EIP-712 digest of a PermitWitnessTransferFrom — same as
digest/2.
@spec upto_domain(map()) :: {:ok, X402.EIP712.domain()} | {:error, domain_error()}
Derives the canonical Permit2 EIP-712 domain — same as domain/1.
Examples
iex> X402.Permit2.upto_domain(%{"network" => "eip155:84532"})
{:ok,
%{
name: "Permit2",
chain_id: 84532,
verifying_contract: "0x000000000022D473030F116dDEE9F6B43aC78BA3"
}}
iex> X402.Permit2.upto_domain(%{"network" => "solana:mainnet"})
{:error, :unsupported_network}
@spec upto_proxy_address() :: String.t()
Returns the x402UptoPermit2Proxy contract address — the upto spender.
Deployed at the same address on every supported EVM chain via CREATE2.
Examples
iex> X402.Permit2.upto_proxy_address()
"0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002"