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

Local verification of EVM `exact` (EIP-3009 and Permit2) and `upto`
(Permit2) payment payloads.

Runs the full facilitator verify checklist from the exact-EVM and
upto-EVM scheme specifications locally, so an Elixir resource server can
cryptographically verify payments instead of trusting a remote
facilitator's `verify` endpoint. The checks mirror the reference
TypeScript/Go/Python facilitator engines check for check.

## Payment kinds

The requirements select the flow; the result's `kind` echoes it:

* `:eip3009` — `exact` with `extra.assetTransferMethod` absent or
  `"eip3009"`; the scheme payload carries `authorization`.
* `:permit2_exact` — `exact` with `extra.assetTransferMethod` `"permit2"`;
  the payload carries `permit2Authorization`, the `spender` must be the
  `x402ExactPermit2Proxy`, the witness `to` must equal `payTo`, the
  `permitted.amount` must equal `amount` and `permitted.token` the
  `asset`, and the `deadline`/`witness.validAfter` window must cover now.
* `:permit2_upto` — `upto`; as `:permit2_exact` but the spender is the
  `x402UptoPermit2Proxy`, the witness additionally binds
  `extra.facilitatorAddress`, and the requirements' `amount` (what gets
  settled) may be at most `permitted.amount`.

At `:full`, Permit2 flows simulate the proxy's `settle` via `eth_call`
(for `upto` as the witness facilitator, the only sender the proxy
accepts) and diagnose a failure the way the reference facilitator does:
proxy not deployed, insufficient balance, missing ERC-20 allowance to
the canonical Permit2 contract, or a generic simulation failure —
after mapping any named Permit2/proxy custom error first.

## Verification levels

The `:level` option is required and explicit — a level whose capabilities
are unavailable returns an error instead of silently downgrading:

* `:structural` — pure checks, no cryptography and no RPC: scheme,
  network, and EIP-712 domain requirements; payload shape; `payTo`
  recipient equality; exact amount equality; and the
  `validAfter`/`validBefore` window with the reference implementations'
  6-second settlement buffer.

* `:signature` — everything in `:structural`, plus EIP-712 digest
  recomputation and EOA signature recovery (requires the optional
  `ex_keccak` and `ex_secp256k1` dependencies, otherwise
  `{:error, :missing_dependency}`). Smart-wallet signatures (ERC-1271 /
  ERC-6492) cannot be proven without RPC and are rejected with
  `{:error, {:invalid, :smart_wallet_requires_rpc}}` — fail closed, never
  assume.

* `:full` — everything in `:signature`, plus on-chain checks over a
  configured `X402.RPC` endpoint (otherwise
  `{:error, :rpc_not_configured}`): chain-id cross-check, signature
  routing by payer bytecode (EOA `ecrecover` when the payer has no code,
  strict ERC-1271 `isValidSignature` when it does — no ECDSA fallback,
  matching on-chain `SignatureChecker` semantics), ERC-6492 counterfactual
  handling, asset bytecode presence, `balanceOf` funding, and an
  `eth_call` simulation of `transferWithAuthorization` with failure
  diagnosis (nonce already used, insufficient balance, token domain
  mismatch, ...).

## ERC-6492 counterfactual signatures (fail-closed)

A wrapped signature from an undeployed wallet is **never** accepted on the
strength of the wrapper alone (the reference Go design):

* the deployment factory must appear in `:eip6492_allowed_factories`
  (default `[]` — all counterfactual payments are rejected with
  `{:invalid, :eip6492_factory_not_allowed}` until factories are
  explicitly trusted), and
* validity is proven only by an atomic Multicall3 simulation that deploys
  the wallet and executes the transfer in a single `eth_call`. With
  `simulate: false` counterfactual payments are rejected with
  `{:invalid, :undeployed_smart_wallet}`.

## Example

    {:ok, payload} = X402.PaymentSignature.decode_and_validate(header, requirements)

    {:ok, rpc} = X402.RPC.new(rpc_url: "https://sepolia.base.org", finch: MyApp.Finch)

    case X402.Verify.EVM.verify(payload, requirements, level: :full, rpc: rpc) do
      {:ok, %{payer: payer}} -> grant_access(payer)
      {:error, _reason} -> deny_access()
    end

The result never silently downgrades: `{:ok, result}` means the payment
passed every check the stated `:level` includes, and `result.level` echoes
that level.

Structural-only verification is pure and needs no optional dependency:

    iex> requirements = %{
    ...>   "scheme" => "exact",
    ...>   "network" => "eip155:84532",
    ...>   "amount" => "10000",
    ...>   "asset" => "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
    ...>   "payTo" => "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
    ...>   "maxTimeoutSeconds" => 60,
    ...>   "extra" => %{"name" => "USDC", "version" => "2"}
    ...> }
    iex> payload = %{
    ...>   "x402Version" => 2,
    ...>   "accepted" => requirements,
    ...>   "payload" => %{
    ...>     "signature" => "0x" <> String.duplicate("11", 65),
    ...>     "authorization" => %{
    ...>       "from" => "0x857b06519E91e3A54538791bDbb0E22373e36b66",
    ...>       "to" => "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
    ...>       "value" => "10000",
    ...>       "validAfter" => "0",
    ...>       "validBefore" => "32503680000",
    ...>       "nonce" => "0x" <> String.duplicate("ab", 32)
    ...>     }
    ...>   }
    ...> }
    iex> {:ok, result} = X402.Verify.EVM.verify(payload, requirements, level: :structural)
    iex> {result.level, result.payer}
    {:structural, "0x857b06519e91e3a54538791bdbb0e22373e36b66"}

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

Local-only reasons without a canonical wire equivalent fall back to their
atom name.

## Examples

    iex> X402.Verify.EVM.reason_string(:recipient_mismatch)
    "invalid_exact_evm_recipient_mismatch"

    iex> X402.Verify.EVM.reason_string(:nonce_already_used)
    "invalid_exact_evm_nonce_already_used"

    iex> X402.Verify.EVM.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 a capability error (`:missing_dependency`,
`: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 nothing, `:signature` needs the
  optional crypto dependencies, `:full` additionally needs `:rpc`. A
  level never silently downgrades.

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

* `:simulate` - Whether level `:full` simulates `transferWithAuthorization` via
  `eth_call`. Counterfactual ERC-6492 payments always require simulation
  and are rejected when it is `false`; `:counterfactual_only` skips the
  EOA/ERC-1271 transfer simulation but keeps the atomic counterfactual
  deploy-and-transfer simulation, which is the only possible proof of a
  counterfactual signature. The default value is `true`.

* `:verify_chain_id` (`t:boolean/0`) - Whether level `:full` cross-checks `eth_chainId` against the CAIP-2
  network in the requirements, guarding against a misconfigured RPC
  endpoint. Adds no extra round-trip (batched with the other reads). The default value is `true`.

* `:eip6492_allowed_factories` (list of `t:String.t/0`) - Factory contract addresses trusted to deploy counterfactual ERC-6492
  smart wallets (case-insensitive). The default empty list rejects every
  counterfactual payment. The default value is `[]`.

* `:multicall_address` (`t:String.t/0`) - The Multicall3 contract used for atomic ERC-6492 deploy-and-transfer simulation. The default value is `"0xcA11bde05977b3631167028862bE2a173976CA11"`.

## Examples

    iex> X402.Verify.EVM.verify(%{"x402Version" => 2}, %{}, level: :structural)
    {:error, {:invalid, :invalid_payload}}

# `error`

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

Verification errors.

# `invalid_reason`

```elixir
@type invalid_reason() ::
  :invalid_payload
  | :invalid_authorization
  | :invalid_requirements
  | :scheme_mismatch
  | :unsupported_transfer_method
  | :unsupported_network
  | :network_mismatch
  | :missing_eip712_domain
  | :recipient_mismatch
  | :value_mismatch
  | :valid_before_expired
  | :valid_after_in_future
  | :invalid_signature
  | :smart_wallet_requires_rpc
  | :undeployed_smart_wallet
  | :eip6492_factory_not_allowed
  | :asset_not_deployed_contract
  | :balance_check_failed
  | :insufficient_balance
  | :eip3009_not_supported
  | :nonce_already_used
  | :token_name_mismatch
  | :token_version_mismatch
  | :simulation_failed
  | :upto_scheme_mismatch
  | :upto_network_mismatch
  | :upto_facilitator_mismatch
  | :settlement_exceeds_amount
  | :invalid_permit2_spender
  | :permit2_recipient_mismatch
  | :permit2_deadline_expired
  | :permit2_not_yet_valid
  | :permit2_amount_mismatch
  | :permit2_token_mismatch
  | :invalid_permit2_signature
  | :permit2_proxy_not_deployed
  | :permit2_insufficient_balance
  | :permit2_allowance_required
  | :permit2_simulation_failed
  | :permit2_2612_amount_mismatch
  | :permit2_invalid_amount
  | :permit2_invalid_destination
  | :permit2_invalid_owner
  | :permit2_payment_too_early
  | :permit2_invalid_nonce
  | :upto_amount_exceeds_permitted
  | :upto_unauthorized_facilitator
```

Why a payment was rejected.

Reasons map onto the canonical cross-SDK `invalidReason` strings via
`reason_string/1` where an equivalent exists. The `permit2_*` and
`upto_*` reasons are produced by the Permit2 flows only.

# `kind`

```elixir
@type kind() :: :eip3009 | :permit2_exact | :permit2_upto
```

The payment flow a payload belongs to.

# `level`

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

Requested verification depth.

# `signature_type`

```elixir
@type signature_type() :: :eoa | :erc1271 | :erc6492_counterfactual
```

How the payment signature was (or would be) verified.

# `simulate`

```elixir
@type simulate() :: boolean() | :counterfactual_only
```

Simulation mode for level `:full`.

`:counterfactual_only` skips the EOA/ERC-1271 transfer simulation like
`false`, but keeps the atomic ERC-6492 deploy-and-transfer simulation —
the only possible signature proof for an undeployed wallet.

# `verification`

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

A successful verification.

`payer` is the lowercase authorization `from` address. `kind` is the
payment flow (see the module documentation). `level` echoes the level
that was run. `signature_type` is `nil` at `:structural` (no signature
classification happens without cryptography).

---

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