Builds and validates the erc20ApprovalGasSponsoring extension.
The extension enables a gasless Permit2
approval flow for ERC-20 tokens that do not implement EIP-2612. The
client signs — but does not broadcast — a normal EVM transaction calling
approve(Permit2, amount); the facilitator funds the wallet's gas if
needed, broadcasts the approval, and settles via x402Permit2Proxy in one
atomic bundle.
Unlike X402.Extensions.EIP2612GasSponsoring, nothing here is EIP-712
typed data: the signed artifact is a full RLP-encoded transaction whose
nonce must match the wallet's current on-chain nonce and whose fees must
match live network prices. Producing it therefore requires chain access
and transaction-signing tooling outside this library — this module builds,
validates, and attaches the extension data around a pre-signed
transaction supplied by the caller.
Server side, build_extension/0 declares support under
extensions.erc20ApprovalGasSponsoring in a PAYMENT-REQUIRED response,
and extract_info/1 / validate_info/1 check the client-populated data
echoed back in a PaymentPayload.
Client side, build_info/1 assembles the wire info for a pre-signed
approval transaction, put_info/2 attaches it to a payload's extensions,
and enricher/1 packages both for X402.Client.build_payment/3:
{:ok, payload} =
X402.Client.build_payment(payment_required, signer,
extensions: [
X402.Extensions.ERC20ApprovalGasSponsoring.enricher(
from: wallet_address,
signed_transaction: signed_approve_tx_hex
)
]
)
Summary
Types
Client-populated extension info in wire shape: string keys "from",
"asset", "spender", "amount", "signedTransaction", and
"version".
A built extensions.erc20ApprovalGasSponsoring declaration (info + schema).
Functions
Builds the server-side extension declaration (info + schema).
Builds the client-populated info for a pre-signed approval transaction.
Returns an enricher for X402.Client.build_payment/3's :extensions.
Extracts the client-populated info from a PaymentPayload map.
Returns the extension key, "erc20ApprovalGasSponsoring".
Returns MaxUint256 as a decimal string — the default approval amount.
Returns the canonical Permit2 contract address — the default spender.
Attaches client-populated info to a payload's extensions.
Returns the JSON Schema (Draft 2020-12) for the client-populated info.
Validates the format of client-populated info.
Types
Client-populated extension info in wire shape: string keys "from",
"asset", "spender", "amount", "signedTransaction", and
"version".
A built extensions.erc20ApprovalGasSponsoring declaration (info + schema).
Functions
@spec build_extension() :: t()
Builds the server-side extension declaration (info + schema).
Resource servers advertise support by placing the declaration under
extensions.erc20ApprovalGasSponsoring in a PAYMENT-REQUIRED response;
the client populates the actual approval data.
Examples
iex> ext = X402.Extensions.ERC20ApprovalGasSponsoring.build_extension()
iex> ext["info"]["version"]
"1"
iex> ext["schema"]["required"]
["from", "asset", "spender", "amount", "signedTransaction", "version"]
Builds the client-populated info for a pre-signed approval transaction.
Validates the declared fields structurally (the transaction itself is
opaque to this library — the facilitator decodes and verifies it against
the declared from, asset, spender, and amount) and returns the
wire-shaped info, ready for put_info/2.
Options
:from(String.t/0) - Required. The address of the wallet that signed the approval transaction.:signed_transaction(String.t/0) - Required. The RLP-encoded signed EIP-1559 transaction callingapprove(spender, amount), as a0x-prefixed hex string. Its signer, target contract, calldata, nonce, and fees are verified on-chain by the facilitator.:amount- The approval amount declared alongside the transaction, in atomic units. Must match the amount in the transaction's calldata. Defaults to MaxUint256, matching the reference client implementations.:spender(String.t/0) - The approved spender declared alongside the transaction. Must match the spender in the transaction's calldata. Defaults to the canonical Permit2 contract.:asset(String.t/0) - Required. The ERC-20 token contract the transaction approves.
Examples
iex> {:ok, info} =
...> X402.Extensions.ERC20ApprovalGasSponsoring.build_info(
...> from: "0x857b06519E91e3A54538791bDbb0E22373e36b66",
...> asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
...> signed_transaction: "0x02f8" <> String.duplicate("ab", 100)
...> )
iex> info["spender"]
"0x000000000022D473030F116dDEE9F6B43aC78BA3"
iex> info["version"]
"1"
iex> X402.Extensions.ERC20ApprovalGasSponsoring.build_info(
...> from: "0x123",
...> asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
...> signed_transaction: "0xabcd"
...> )
{:error, {:invalid_info_field, "from"}}
@spec enricher(keyword()) :: (map(), map() | nil -> {:ok, map()} | {:error, {:invalid_info_field, String.t()} | :invalid_requirements})
Returns an enricher for X402.Client.build_payment/3's :extensions.
The enricher attaches the pre-signed approval transaction via
build_info/1 and put_info/2 — but only when the server advertised
erc20ApprovalGasSponsoring in the PaymentRequired extensions;
otherwise the payload passes through unchanged. :asset defaults to the
accepted requirements' asset.
Options
:from(String.t/0) - Required. The address of the wallet that signed the approval transaction.:signed_transaction(String.t/0) - Required. The RLP-encoded signed EIP-1559 transaction callingapprove(spender, amount), as a0x-prefixed hex string. Its signer, target contract, calldata, nonce, and fees are verified on-chain by the facilitator.:amount- The approval amount declared alongside the transaction, in atomic units. Must match the amount in the transaction's calldata. Defaults to MaxUint256, matching the reference client implementations.:spender(String.t/0) - The approved spender declared alongside the transaction. Must match the spender in the transaction's calldata. Defaults to the canonical Permit2 contract.:asset(String.t/0) - The ERC-20 token contract the transaction approves. Defaults to the accepted payment requirements'asset.
@spec extract_info(map()) :: {:ok, info()} | {:error, info_error()}
Extracts the client-populated info from a PaymentPayload map.
Returns the info when the extension is present and every required field
is populated. Field formats are not checked here — see validate_info/1.
Examples
iex> X402.Extensions.ERC20ApprovalGasSponsoring.extract_info(%{"payload" => %{}})
{:error, :extension_missing}
@spec key() :: String.t()
Returns the extension key, "erc20ApprovalGasSponsoring".
Examples
iex> X402.Extensions.ERC20ApprovalGasSponsoring.key()
"erc20ApprovalGasSponsoring"
@spec max_uint256() :: String.t()
Returns MaxUint256 as a decimal string — the default approval amount.
Examples
iex> X402.Extensions.ERC20ApprovalGasSponsoring.max_uint256()
"115792089237316195423570985008687907853269984665640564039457584007913129639935"
@spec permit2_address() :: String.t()
Returns the canonical Permit2 contract address — the default spender.
Examples
iex> X402.Extensions.ERC20ApprovalGasSponsoring.permit2_address()
"0x000000000022D473030F116dDEE9F6B43aC78BA3"
Attaches client-populated info to a payload's extensions.
Places the info under extensions.erc20ApprovalGasSponsoring.info,
following the append-only rule: when the payload already echoes the
server's declaration, server-declared info fields are preserved (they win
over client values) and the declared schema is kept; the client's
fields are added alongside them.
Examples
iex> info = %{"from" => "0x1111111111111111111111111111111111111111"}
iex> payload = X402.Extensions.ERC20ApprovalGasSponsoring.put_info(%{"payload" => %{}}, info)
iex> payload["extensions"]["erc20ApprovalGasSponsoring"]["info"]["from"]
"0x1111111111111111111111111111111111111111"
@spec schema() :: map()
Returns the JSON Schema (Draft 2020-12) for the client-populated info.
@spec validate_info(term()) :: :ok | {:error, info_error()}
Validates the format of client-populated info.
Checks that addresses match ^0x[a-fA-F0-9]{40}$, that amount is a
decimal string, that signedTransaction is a 0x-prefixed hex string,
and that version is a dotted numeric version.
Examples
iex> X402.Extensions.ERC20ApprovalGasSponsoring.validate_info(%{
...> "from" => "0x857b06519E91e3A54538791bDbb0E22373e36b66",
...> "asset" => "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
...> "spender" => "0x000000000022D473030F116dDEE9F6B43aC78BA3",
...> "amount" => "10000",
...> "signedTransaction" => "0xabcdef",
...> "version" => "1"
...> })
:ok
iex> X402.Extensions.ERC20ApprovalGasSponsoring.validate_info(%{"from" => "0x123"})
{:error, {:invalid_info_field, "from"}}