Permit2 PermitWitnessTransferFrom building and EIP-712 signing for the
x402 upto scheme on EVM networks.
Implements the client half of the upto scheme: 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).
The upto authorization
Per the upto-EVM scheme specification, the client signs a
PermitWitnessTransferFrom message for the canonical
Permit2 contract
(0x000000000022D473030F116dDEE9F6B43aC78BA3) where
permitted.token/permitted.amountare the requirements'assetandamount— the amount is the maximum the client authorizes; the server settles for the actual usage, up to this ceiling;spenderis thex402UptoPermit2Proxycontract (0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002), deployed at the same address on every supported EVM chain;- the witness struct
Witness(address to,address facilitator,uint256 validAfter)binds the requirements'payTo(to) and the facilitator announced inextra.facilitatorAddress(facilitator), so no other party can settle the authorization. Facilitators announce their address viaGET /supported(X402.Facilitator.supported/1), and resource servers forward it inside each upto requirements entry'sextra.
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
Types
A Permit2 PermitWitnessTransferFrom authorization in wire shape.
The scheme payload map for a signed upto Permit2 payment.
Functions
Builds an upto PermitWitnessTransferFrom authorization in wire shape.
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 upto Permit2 scheme payload for the given requirements.
Computes the EIP-712 digest of an upto PermitWitnessTransferFrom.
Derives the canonical Permit2 EIP-712 domain from v2 payment requirements.
Returns the x402UptoPermit2Proxy contract address — the upto spender.
Types
A Permit2 PermitWitnessTransferFrom authorization in wire shape.
String keys "from", "permitted" (%{"token", "amount"}),
"spender", "nonce", "deadline", and "witness"
(%{"to", "facilitator", "validAfter"}).
@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 upto Permit2 payment.
Functions
@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.
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.
Returns the 0x-prefixed 65-byte r || s || v signature.
@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 an upto 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). 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 upto_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.
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"