Built-in X402.Scheme for exact payments on Solana (solana:*) networks.
Follows the x402 exact SVM scheme specification: the client builds a
version 0 Solana transaction with the reference instruction layout —
- Compute Budget
SetComputeUnitLimit - Compute Budget
SetComputeUnitPrice - SPL Token / Token-2022
TransferCheckedto the Associated Token Account derived frompayToandasset - SPL Memo (the seller's
extra.memo, or a random nonce for transaction uniqueness)
— signs it with the payer's Ed25519 key, and leaves the fee payer's
signature slot as a 64-byte zero placeholder (a partially signed
transaction). The wire payload is %{"transaction" => base64}, exactly
as the reference TypeScript and Python clients produce.
Roles
- Client —
sign/3builds and partially signs the transaction. The signer must implement the optionalX402.Signer.sign_ed25519/2callback (X402.Signer.SolanaKeydoes). An entry is signable whenextra.feePayeris present: the sponsor's public key is required (it becomes account 0 / the empty signature slot). - Server —
validate_payload/3runs structural checks that any validexactSVM payment must satisfy (decodable Base64, the 1232-byte transaction size cap, a parseable v0/legacy transaction, fee payer match).precheck/3additionally enforces the facilitator's static verification path whitelist (spec §3.1): 3–7 instructions in the reference order, compute-budget bounds, fee payer isolation, transfer semantics against the requirements, and memo enforcement.
On-chain verification and settlement remain facilitator-delegated:
this module never talks to a Solana RPC node. The facilitator resolves
address lookup tables, simulates, signs as feePayer, and submits
(spec §2–3). Transactions using address lookup tables skip precheck/3
(the account set cannot be resolved locally) and are left to the
facilitator, and smart-wallet (CPI-wrapped) payments — the spec's opt-in
Path 2 — will fail the static-path pre-checks; gates fronting a
facilitator with enableSmartWalletVerification should disable
:local_prechecks for those routes.
Client options
sign/3 honors these X402.Client.build_payment/3 options:
:svm_blockhash— a Base58 recent blockhash for the transaction lifetime. Used when the requirements'extra.recentBlockhashhint is absent or malformed; keeps the module RPC-free.:svm_blockhash_fetcher— a 1-arity fun receiving the CAIP-2 network and returning{:ok, blockhash}(for example a wrapper around your RPC client'sgetLatestBlockhash). Consulted after:svm_blockhash.:svm_decimals/:svm_token_program— the mint's decimals and owning token program, needed byTransferChecked. Defaults come from the reference SDKs' known-asset table (USDC, USDT, USDG, PYUSD, CASH); for other mints pass both explicitly (production clients read them from the mint account via RPC).
Blockhash resolution order (per the spec): a valid
extra.recentBlockhash from the server wins, then :svm_blockhash,
then :svm_blockhash_fetcher; with none, {:error, :missing_blockhash}.
Summary
Types
Reasons precheck/3 fails fast with {:error, {:precheck_failed, reason}}.
Functions
Returns ["solana:*"] — every Solana network.
Static-path pre-checks (spec §3.1) before the facilitator round-trip.
Returns "exact".
Builds and partially signs the SVM exact transaction.
Whether the entry carries the required sponsor data.
Structural validation of a decoded PAYMENT-SIGNATURE payload.
Types
@type precheck_failure() ::
:invalid_transaction
| :fee_payer_not_isolated
| :instruction_count
| :invalid_compute_limit_instruction
| :invalid_compute_price_instruction
| :compute_price_too_high
| :missing_transfer_instruction
| :amount_mismatch
| :mint_mismatch
| :recipient_mismatch
| :unknown_optional_instruction
| :memo_count
| :memo_mismatch
Reasons precheck/3 fails fast with {:error, {:precheck_failed, reason}}.
Functions
@spec networks() :: [String.t()]
Returns ["solana:*"] — every Solana network.
Examples
iex> X402.Scheme.ExactSVM.networks()
["solana:*"]
@spec precheck(map(), map(), keyword()) :: :ok | {:error, {:precheck_failed, precheck_failure()}}
Static-path pre-checks (spec §3.1) before the facilitator round-trip.
Enforces the facilitator's static verification whitelist as far as it is
verifiable without RPC: 3–7 top-level instructions in the reference
order (SetComputeUnitLimit, SetComputeUnitPrice, TransferChecked,
then only Lighthouse/Memo), the ≤ 5 lamports/CU priority-fee cap, fee
payer isolation (§2.1.1 — the fee payer referenced by no instruction),
transfer semantics against the requirements (amount equality, mint,
destination ATA), and memo enforcement when extra.memo is present.
Transactions using address lookup tables pass through with :ok — their
account set cannot be resolved without RPC, so the facilitator remains
the authority (§2.1.2). Failures return
{:error, {:precheck_failed, reason}} and the gate answers 402 without
a facilitator call.
@spec scheme() :: String.t()
Returns "exact".
Examples
iex> X402.Scheme.ExactSVM.scheme()
"exact"
@spec sign(map(), X402.Signer.t(), keyword()) :: {:ok, map()} | {:error, term()}
Builds and partially signs the SVM exact transaction.
Returns {:ok, %{"transaction" => base64}} — the wire scheme payload —
or a structured error: {:error, :missing_fee_payer} when the
requirements lack extra.feePayer, {:error, :missing_blockhash} when
no blockhash source is available, {:error, {:unknown_asset, mint}}
for mints outside the known-asset table without explicit
:svm_decimals/:svm_token_program, and {:error, :unsupported_signer}
when the signer cannot sign Ed25519.
Whether the entry carries the required sponsor data.
extra.feePayer is required by the SVM exact scheme (the sponsor's
public key becomes the transaction's fee payer), and asset/payTo
must be valid Solana addresses.
Examples
iex> X402.Scheme.ExactSVM.signable?(%{
...> "asset" => "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
...> "payTo" => "GyGKxMyg1p9SsHfm15MkNUu1u9TN2JtTspcdmrtGUdse",
...> "extra" => %{"feePayer" => "9hSR6S7WPtxmTojgo6GG3k4yDPecgJY292j7xrsUGWBu"}
...> })
true
iex> X402.Scheme.ExactSVM.signable?(%{"extra" => %{}})
false
@spec validate_payload(map(), map(), keyword()) :: :ok | {:error, {:invalid_scheme_payment, atom()}}
Structural validation of a decoded PAYMENT-SIGNATURE payload.
Checks what any valid exact SVM payment must satisfy without RPC:
payload.transaction present, Base64-decodable, within the network's
1232-byte transaction size cap, parseable as a v0 or legacy Solana
transaction, and — when the requirements advertise extra.feePayer —
carrying that fee payer as account 0. Failures return
{:error, {:invalid_scheme_payment, reason}}.
Examples
iex> X402.Scheme.ExactSVM.validate_payload(%{"payload" => %{}}, %{}, [])
{:error, {:invalid_scheme_payment, :missing_transaction}}
iex> X402.Scheme.ExactSVM.validate_payload(
...> %{"payload" => %{"transaction" => "!!!"}},
...> %{},
...> []
...> )
{:error, {:invalid_scheme_payment, :invalid_base64}}