# `X402.Verify.SVM`
[🔗](https://github.com/cardotrejos/x402/blob/v0.9.0/lib/x402/verify/svm.ex#L1)

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`, and `payTo` (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`, plus `simulateTransaction` over
  a configured `X402.RPC` endpoint (otherwise
  `{:error, :rpc_not_configured}`). The simulation runs with
  `sigVerify: 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()
    end

`payer` 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.

# `reason_string`
*since 0.6.0* 

```elixir
@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"

# `verify`
*since 0.6.0* 

```elixir
@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. `:structural` needs no RPC; `:full` additionally
  simulates the transaction (requires `:rpc` unless `simulate: false`).
  A level never silently downgrades.

* `:fee_payer` (`t:String.t/0`) - Required. The facilitator-managed fee-payer address (Base58). The requirements'
  `extra.feePayer` must equal it — a facilitator must never co-sign a
  transaction whose fee payer it does not control.

* `:rpc` - An `X402.RPC` configuration. Required for level `:full` with simulation.

* `:simulate` (`t:boolean/0`) - Whether level `:full` runs `simulateTransaction`. The default value is `true`.

* `:max_required_signatures` - Cap on the transaction's required signature count (every signature
  adds 5000 lamports of base fee, paid by the facilitator). `nil`
  disables the cap. A typical x402 payment needs two. The default value is `nil`.

* `:commitment` (`t: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}}

# `error`

```elixir
@type error() ::
  {:invalid, invalid_reason()}
  | :rpc_not_configured
  | {:rpc_error, X402.RPC.error()}
```

Verification errors.

# `invalid_reason`

```elixir
@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.

# `level`

```elixir
@type level() :: :structural | :full
```

Requested verification depth.

# `verification`

```elixir
@type verification() :: %{payer: String.t(), level: level()}
```

A successful verification.

`payer` is the Base58 `TransferChecked` authority. `level` echoes the
level that was run.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
