Local verification of SVM exact payment payloads.
Runs the facilitator verify checklist from the exact-SVM scheme
specification's static verification path (Path 1), mirroring the
reference TypeScript facilitator check for check: scheme/network match,
fee-payer requirements, transaction decoding, local Ed25519 verification
of every required signer except the fee payer, the static instruction
whitelist (via X402.Scheme.ExactSVM's pre-checks), and — at :full —
an RPC simulateTransaction round-trip.
Smart-wallet (CPI-wrapped) payments — the spec's opt-in Path 2 — are out of scope and fail the static checks, and transactions using address lookup tables are rejected fail-closed (their account set cannot be verified without table resolution).
Verification levels
The :level option is required and explicit — a level whose capabilities
are unavailable returns an error instead of silently downgrading:
:structural— no RPC: scheme, network, and fee-payer requirements; transaction decoding; Ed25519 signature verification of the required signers (pure:crypto, no optional dependency); the address-lookup- table rejection; fail-closed validation of the requirements'amount,asset, andpayTo(an uninterpretable field is rejected, never skipped); and the static instruction-layout checks against the requirements (amount, mint, destination ATA, memo, compute budget bounds, fee-payer isolation).:full— everything in:structural, plussimulateTransactionover a configuredX402.RPCendpoint (otherwise{:error, :rpc_not_configured}). The simulation runs withsigVerify: false— the fee-payer slot is unsigned until settlement — which is exactly why the local signature checks above are mandatory at every level.
Example
{:ok, payload} = X402.PaymentSignature.decode_and_validate(header, requirements)
{:ok, rpc} = X402.RPC.new(rpc_url: "https://api.devnet.solana.com", finch: MyApp.Finch)
case X402.Verify.SVM.verify(payload, requirements,
level: :full,
rpc: rpc,
fee_payer: facilitator_address
) do
{:ok, %{payer: payer}} -> grant_access(payer)
{:error, _reason} -> deny_access()
endpayer is the Base58 authority of the TransferChecked instruction —
the account whose tokens move. The result never silently downgrades:
{:ok, result} means the payment passed every check the stated :level
includes, and result.level echoes that level.
Summary
Payment Verification
Maps an invalid reason atom to the canonical cross-SDK invalidReason
string used by the reference facilitators.
Verifies a decoded v2 PaymentPayload against payment requirements.
Types
Verification errors.
Why a payment was rejected.
Requested verification depth.
A successful verification.
Payment Verification
@spec reason_string(invalid_reason()) :: String.t()
Maps an invalid reason atom to the canonical cross-SDK invalidReason
string used by the reference facilitators.
The vocabulary is the TypeScript reference's invalid_exact_svm_* set —
the strings the hosted facilitator emits. (The Go SDK currently diverges
with an invalid_exact_solana_* prefix despite its cross-SDK parity
comment; TypeScript is authoritative here.) Local-only reasons without a
canonical wire equivalent fall back to their atom name.
Examples
iex> X402.Verify.SVM.reason_string(:amount_mismatch)
"invalid_exact_svm_payload_amount_mismatch"
iex> X402.Verify.SVM.reason_string(:fee_payer_mismatch)
"invalid_exact_svm_fee_payer_mismatch"
iex> X402.Verify.SVM.reason_string(:invalid_payload)
"invalid_payload"
@spec verify(map(), map(), keyword()) :: {:ok, verification()} | {:error, error()}
Verifies a decoded v2 PaymentPayload against payment requirements.
payment_payload is the decoded v2 envelope (as returned by
X402.PaymentSignature.decode_and_validate/2) and requirements the
matched PaymentRequirements object. Both accept string or atom keys.
Returns {:ok, verification} when the payment passes every check the
stated :level includes, {:error, {:invalid, reason}} when a check
fails, and {:error, :rpc_not_configured} when the level cannot run —
never a silent downgrade. RPC transport failures return
{:error, {:rpc_error, reason}} (fail closed: the payment is not proven
valid).
Options
:level- Required. Verification depth.:structuralneeds no RPC;:fulladditionally simulates the transaction (requires:rpcunlesssimulate: false). A level never silently downgrades.:fee_payer(String.t/0) - Required. The facilitator-managed fee-payer address (Base58). The requirements'extra.feePayermust equal it — a facilitator must never co-sign a transaction whose fee payer it does not control.:rpc- AnX402.RPCconfiguration. Required for level:fullwith simulation.:simulate(boolean/0) - Whether level:fullrunssimulateTransaction. The default value istrue.:max_required_signatures- Cap on the transaction's required signature count (every signature adds 5000 lamports of base fee, paid by the facilitator).nildisables the cap. A typical x402 payment needs two. The default value isnil.:commitment(String.t/0) - Commitment level for the simulation. The default value is"confirmed".
Examples
iex> X402.Verify.SVM.verify(%{"x402Version" => 2}, %{},
...> level: :structural,
...> fee_payer: "9hSR6S7WPtxmTojgo6GG3k4yDPecgJY292j7xrsUGWBu"
...> )
{:error, {:invalid, :invalid_payload}}
Types
@type error() :: {:invalid, invalid_reason()} | :rpc_not_configured | {:rpc_error, X402.RPC.error()}
Verification errors.
@type invalid_reason() ::
:invalid_payload
| :unsupported_scheme
| :network_mismatch
| :missing_fee_payer
| :fee_payer_not_managed
| :transaction_could_not_be_decoded
| :fee_payer_mismatch
| :excessive_signers
| :signature_invalid
| :alt_resolution_not_available
| :invalid_requirements_amount
| :invalid_requirements_asset
| :invalid_requirements_pay_to
| :instruction_count
| :invalid_compute_limit_instruction
| :invalid_compute_price_instruction
| :compute_price_too_high
| :missing_transfer_instruction
| :fee_payer_not_isolated
| :amount_mismatch
| :mint_mismatch
| :recipient_mismatch
| :unknown_optional_instruction
| :memo_count
| :memo_mismatch
| :verification_failed
| :simulation_failed
Why a payment was rejected.
Reasons map onto the canonical cross-SDK invalidReason strings via
reason_string/1 where an equivalent exists.
@type level() :: :structural | :full
Requested verification depth.
A successful verification.
payer is the Base58 TransferChecked authority. level echoes the
level that was run.