X402.Permit2 (X402 v0.9.0)

Copy Markdown View Source

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 the x402ExactPermit2Proxy (0x402085c248EeA27D92E8b30b2C58ed07f9E20001) and the witness Witness(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 witness Witness(address to,address facilitator,uint256 validAfter) binds payTo and the facilitator announced in extra.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

exact_settle_calldata(authorization, signature)

(since 0.9.0)
@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"}}

upto_settle_calldata(authorization, amount, signature)

(since 0.9.0)
@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

authorization()

@type authorization() :: %{optional(String.t()) => String.t() | map()}

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).

authorization_error()

@type authorization_error() :: :invalid_requirements | {:missing_extra, String.t()}

domain_error()

@type domain_error() :: :invalid_requirements | :unsupported_network

encode_error()

@type encode_error() :: X402.EIP712.encode_error()

payload()

@type payload() :: %{optional(String.t()) => String.t() | authorization()}

The scheme payload map for a signed Permit2 payment.

Functions

build_exact_authorization(requirements, from)

(since 0.9.0)
@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.

build_upto_authorization(requirements, from)

(since 0.6.0)
@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.

digest(domain, authorization)

(since 0.9.0)
@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).

domain(requirements)

(since 0.9.0)
@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}

exact_proxy_address()

(since 0.9.0)
@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"

facilitator_address(requirements)

(since 0.6.0)
@spec facilitator_address(map()) ::
  {:ok, String.t()} | {:error, {:missing_extra, String.t()}}

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"}}

permit2_address()

(since 0.6.0)
@spec permit2_address() :: String.t()

Returns the canonical Permit2 contract address.

Examples

iex> X402.Permit2.permit2_address()
"0x000000000022D473030F116dDEE9F6B43aC78BA3"

random_nonce()

(since 0.6.0)
@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

sign_authorization(signer, domain, authorization)

(since 0.6.0)
@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.

sign_exact(requirements, signer)

(since 0.9.0)
@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
  )

sign_upto(requirements, signer)

(since 0.6.0)
@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
  )

upto_digest(domain, authorization)

(since 0.6.0)
@spec upto_digest(map(), map()) :: {:ok, <<_::256>>} | {:error, encode_error()}

Computes the EIP-712 digest of a PermitWitnessTransferFrom — same as digest/2.

upto_domain(requirements)

(since 0.6.0)
@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}

upto_proxy_address()

(since 0.6.0)
@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"