# `X402.Facilitator.SVMEngine`
[🔗](https://github.com/cardotrejos/x402/blob/v0.9.0/lib/x402/facilitator/svm_engine.ex#L1)

Facilitator-role engine for `exact` payments on Solana (`solana:*`).

The SVM counterpart of `X402.Facilitator.Engine`: it assembles the SDK's
local SVM verification (`X402.Verify.SVM`), Solana JSON-RPC calls
(`X402.Solana.RPC`), transaction primitives (`X402.Solana.Transaction`),
and the signer behaviour (`X402.Signer`) into the engine behind the
facilitator API's three operations, speaking the same wire shapes as the
reference facilitators:

* `verify/3` — the full exact-SVM static-path checklist, returning the
  `POST /verify` response.
* `settle/3` — **re-verifies independently**, co-signs the fee-payer slot
  with the configured signer, broadcasts via `sendTransaction`, and polls
  `getSignatureStatuses` until the transaction is confirmed, returning the
  `POST /settle` response with the Base58 transaction signature.
* `supported/1` — the `GET /supported` response derived from the
  configured networks.

Expose the engine over HTTP with `X402.Plug.Facilitator` (alone, or next
to an EVM `X402.Facilitator.Engine` via the `:engines` option), or call it
directly from your own transport.

## Fee-payer safety

The facilitator's Ed25519 key co-signs client-built transactions, so what
it signs is strictly constrained by verification: the requirements'
`extra.feePayer` must be this engine's signer, account 0 must be that fee
payer, the fee payer must not be referenced by any instruction (isolation
— the sponsor's signature can never move the sponsor's funds), the
instruction layout must match the static whitelist, and every other
required signer's Ed25519 signature is verified locally over the message
bytes. Smart-wallet (CPI-wrapped) payments and address-lookup-table
transactions are rejected fail-closed.

## Settlement pipeline

1. Compute the settlement key — SHA-256 of the transaction's message
   bytes — and **atomically claim it** in the `:settlement_cache`
   (`duplicate_settlement` when already claimed). The claim is released on
   verify failure, node-side broadcast rejection, and terminal on-chain
   failure, and kept on success. On `settlement_pending` the claim is
   kept only when the `:pending_settlement_store` recorded the broadcast
   (a retry then reconciles against the recorded signature); without a
   store it is released, so a retry re-verifies and re-broadcasts the
   identical wire bytes instead of dead-ending on `duplicate_settlement`.
2. Pending-settlement fast path: when a prior settle for this exact
   transaction broadcast but could not confirm, re-await the recorded
   signature instead of re-verifying and re-broadcasting (Solana
   transactions embed an expiring blockhash, so a resend can fail while
   the original is still perfectly valid).
3. Re-verify via `X402.Verify.SVM` at `:full` level, re-simulating by
   default exactly as the reference facilitators do (see
   `:simulate_in_settle`).
4. Sign the message bytes with the configured signer, splice the
   signature into the fee payer's slot 0
   (`X402.Solana.Transaction.attach_signature/3`), and broadcast with
   `skipPreflight: true`.
5. Poll `getSignatureStatuses` until `confirmed`/`finalized` or
   `:confirm_timeout_ms`. A broadcast whose confirmation cannot be
   established returns the spec's non-terminal `"settlement_pending"`
   with the transaction signature and records it in the
   `:pending_settlement_store` for the fast path above.

## Duplicate-settlement protection

> #### Configure a settlement cache and a pending store {: .warning}
>
> With the default `settlement_cache: nil`, duplicate-settlement
> protection is **disabled**: concurrent settles of the same payment all
> broadcast (the network still collapses them to one transaction id, but
> every call burns RPC round-trips and races the confirmation poll).
> Production engines should configure **both** the cache the reference
> facilitators use — 120 seconds, roughly twice the blockhash lifetime —
> and a `:pending_settlement_store`. The two interact: on a
> `settlement_pending` verdict the cache claim is kept only when the
> store recorded the broadcast, letting the retry reconcile against the
> recorded signature; with a cache but no store the claim is released so
> the retry can re-broadcast the identical wire bytes (collapsed by the
> network to one transaction id) instead of being rejected as
> `duplicate_settlement`.
>
>     children = [
>       {X402.Extensions.PaymentIdentifier.ETSCache,
>        name: MyApp.SettlementCache, ttl_ms: 120_000},
>       {X402.Facilitator.PendingSettlementStore.ETS,
>        name: MyApp.PendingSettlements}
>     ]
>
>     settlement_cache:
>       {X402.Extensions.PaymentIdentifier.ETSCache, MyApp.SettlementCache},
>     pending_settlement_store:
>       {X402.Facilitator.PendingSettlementStore.ETS, MyApp.PendingSettlements}

## Example

    {:ok, rpc} = X402.RPC.new(rpc_url: "https://api.devnet.solana.com", finch: MyApp.Finch)
    {:ok, signer} = X402.Signer.SolanaKey.new(System.fetch_env!("SOLANA_FEE_PAYER_KEY"))

    {:ok, engine} =
      X402.Facilitator.SVMEngine.new(
        rpc: rpc,
        signer: signer,
        networks: ["solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"]
      )

    {:ok, %{"isValid" => true, "payer" => payer}} =
      X402.Facilitator.SVMEngine.verify(engine, payment_payload, requirements)

    {:ok, %{"success" => true, "transaction" => signature}} =
      X402.Facilitator.SVMEngine.settle(engine, payment_payload, requirements)

## Hooks and telemetry

`X402.Hooks` callbacks wrap both operations exactly as on
`X402.Facilitator.Engine`, and the same
`[:x402, :facilitator_engine, :verify]` / `[:x402, :facilitator_engine,
:settle]` telemetry events are emitted with `:status` metadata.

# `verify`
*since 0.6.0* 

```elixir
@spec verify(t(), map(), map()) :: {:ok, wire_response()} | {:error, term()}
```

Verifies a payment payload against requirements, returning the
`POST /verify` wire response.

Returns `{:ok, response}` for every *protocol-level* outcome — including
rejected payments, which come back as
`%{"isValid" => false, "invalidReason" => reason, ...}` with the
canonical cross-SDK reason string (`X402.Verify.SVM.reason_string/1`).
`{:error, reason}` is reserved for infrastructure failures (RPC transport
errors, hook crashes) where no verdict about the payment exists;
transports should map it to an opaque 500.

# `settle`
*since 0.6.0* 

```elixir
@spec settle(t(), map(), map()) :: {:ok, wire_response()} | {:error, term()}
```

Settles a payment, returning the `POST /settle` wire response.

Independently re-verifies the payment first, then co-signs the fee-payer
slot and broadcasts — see the module documentation for the full pipeline,
including duplicate-settlement claims and pending-settlement
reconciliation. Rejected or failed settlements come back as
`{:ok, %{"success" => false, "errorReason" => reason, ...}}`; a broadcast
whose confirmation could not be established returns the non-terminal
`"settlement_pending"` reason with the transaction signature.
`{:error, reason}` is reserved for infrastructure failures where nothing
was broadcast.

# `supported`
*since 0.6.0* 

```elixir
@spec supported(t()) :: wire_response()
```

Returns the `GET /supported` wire response for this engine.

One `exact` kind per configured network, no extensions, and the signer's
address under the `solana:*` family. Each kind advertises the fee payer
under `"extra"` — the channel through which reference resource servers
discover which `extra.feePayer` to inject into their 402 challenges
(omitted only when the signer's address is unavailable).

## Examples

    {:ok, engine} = X402.Facilitator.SVMEngine.new(rpc: rpc, signer: signer, networks: [network])
    X402.Facilitator.SVMEngine.supported(engine)
    #=> %{
    #     "kinds" => [
    #       %{
    #         "x402Version" => 2,
    #         "scheme" => "exact",
    #         "network" => network,
    #         "extra" => %{"feePayer" => "9hSR..."}
    #       }
    #     ],
    #     "extensions" => [],
    #     "signers" => %{"solana:*" => ["9hSR..."]}
    #   }

# `t`

```elixir
@type t() :: %X402.Facilitator.SVMEngine{
  confirm_interval_ms: pos_integer(),
  confirm_timeout_ms: pos_integer(),
  hooks: module(),
  max_required_signatures: pos_integer() | nil,
  networks: [String.t()],
  pending_settlement_store:
    X402.Facilitator.PendingSettlementStore.adapter() | nil,
  rpc: X402.RPC.t(),
  settlement_cache: X402.Extensions.PaymentIdentifier.Cache.adapter() | nil,
  signer: X402.Signer.t(),
  simulate: boolean(),
  simulate_in_settle: boolean()
}
```

A validated engine configuration built by `new/1`.

# `wire_response`

```elixir
@type wire_response() :: %{optional(String.t()) =&gt; term()}
```

A facilitator wire response (`/verify` or `/settle` shape).

# `new`
*since 0.6.0* 

```elixir
@spec new(keyword()) :: {:ok, t()} | {:error, NimbleOptions.ValidationError.t()}
```

Builds a validated engine configuration.

## Options

* `:rpc` - Required. An `X402.RPC` configuration pointed at a Solana JSON-RPC node.

* `:signer` - Required. The fee-payer signer (a struct implementing `X402.Signer` with the
  `sign_ed25519/2` callback — `X402.Signer.SolanaKey` does). Its key
  pays transaction fees and co-signs every settlement.

* `:networks` - Required. Non-empty list of CAIP-2 networks this engine serves
  (`solana:<reference>` only). Verify and settle requests for other
  networks are rejected with `invalid_network`.

* `:hooks` - Lifecycle hook module implementing `X402.Hooks`. The default value is `X402.Hooks.Default`.

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

* `:simulate_in_settle` (`t:boolean/0`) - Whether the independent re-verify inside `settle/3` also simulates.
  On by default, matching the reference facilitators, whose settle
  always re-simulates: it is the blockhash-freshness and balance guard
  right before the preflight-skipping broadcast, and the fee payer is
  charged for a transaction that fails on-chain. Disabling it is an
  explicit operator optimization that trades that guard for one fewer
  RPC round-trip per settle. The default value is `true`.

* `:settlement_cache` - Optional `{module, cache}` adapter implementing
  `X402.Extensions.PaymentIdentifier.Cache`, used as the atomic
  duplicate-settlement claim (`duplicate_settlement`). **`nil` disables
  duplicate protection entirely** — see the module documentation for
  the recommended `ETSCache` configuration with `ttl_ms: 120_000`,
  paired with a `:pending_settlement_store`. The default value is `nil`.

* `:pending_settlement_store` - Optional `{module, store}` adapter implementing
  `X402.Facilitator.PendingSettlementStore`. Lets a retried settle for
  the same transaction reconcile against the already-broadcast
  signature instead of re-verifying and re-broadcasting. `nil` disables
  reconciliation: a `settlement_pending` verdict is returned but not
  recorded, and the `:settlement_cache` claim is released so the retry
  can re-broadcast the identical wire bytes rather than dead-end on
  `duplicate_settlement`. The default value is `nil`.

* `:confirm_timeout_ms` (`t:pos_integer/0`) - How long `settle/3` waits for confirmation before returning the
  non-terminal `"settlement_pending"` response. The default value is `30000`.

* `:confirm_interval_ms` (`t:pos_integer/0`) - Interval between `getSignatureStatuses` polls. The default value is `1000`.

* `:max_required_signatures` - Cap on a transaction's required signature count (every signature adds
  5000 lamports of base fee, paid by this engine's key). `nil` disables
  the cap. The default value is `nil`.

---

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