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

Behaviour for pending-settlement reconciliation stores.

When `X402.Facilitator.Engine.settle/3` broadcasts a transaction but cannot
establish its confirmation — the receipt poll times out, or the transport
fails mid-broadcast — it returns the spec's non-terminal
`"settlement_pending"` response. Without a store that knowledge is lost: a
client retrying the identical payment would re-verify and re-broadcast a
second transaction instead of reconciling against the one already in
flight.

A pending-settlement store closes that loop, mirroring the reference SDKs'
`PendingSettlementStore`: before verifying, `settle/3` looks the payment up
here and, on a hit, **deletes the entry and re-awaits the already-broadcast
transaction** instead of broadcasting a new one. The delete-before-reconcile
ordering is load-bearing — a concurrent retry of the same payload misses the
store and falls through to the normal path, where the chain itself rejects
the duplicate (the EIP-3009 authorization nonce can only be consumed once).

Adapters are configured as `{module, store}` tuples where `module`
implements this behaviour and `store` is adapter-specific runtime state (a
pid, a registered name, a connection handle). The bundled adapter is
`X402.Facilitator.PendingSettlementStore.ETS`; multi-instance facilitators
should supply a shared-store adapter (Redis, database) instead — entries
must be visible to whichever instance receives the retry.

## Entries

An entry records what is needed to reconcile:

  * `:transaction` — the `0x`-prefixed transaction hash.
  * `:provenance` — `:node_acknowledged` when the hash was returned by
    `eth_sendRawTransaction`, or `:local_hash` when the transport failed
    mid-broadcast and the hash was computed locally from the signed
    transaction (the node may never have seen it).
  * `:raw_transaction` — the raw signed transaction bytes for
    `:local_hash` entries (`nil` otherwise), kept so operators can inspect
    or manually rebroadcast a transaction the node may have missed. The
    engine itself never rebroadcasts.

Entries must expire on their own (the bundled adapter defaults to five
minutes) — an entry that outlives its transaction's relevance only delays
the terminal verdict of a retry.

## Writing a distributed adapter

    defmodule MyApp.RedisPendingStore do
      @behaviour X402.Facilitator.PendingSettlementStore

      @ttl_ms :timer.minutes(5)

      @impl true
      def put(conn, key, entry) do
        encoded = Base.encode64(:erlang.term_to_binary(entry))

        case Redix.command(conn, ["SET", "x402:pending:" <> key, encoded, "PX", @ttl_ms]) do
          {:ok, "OK"} -> :ok
          {:error, reason} -> {:error, reason}
        end
      end

      @impl true
      def get(conn, key) do
        case Redix.command(conn, ["GET", "x402:pending:" <> key]) do
          {:ok, nil} -> :miss
          {:ok, encoded} -> {:hit, decode(encoded)}
          {:error, reason} -> {:error, reason}
        end
      end

      @impl true
      def delete(conn, key) do
        case Redix.command(conn, ["DEL", "x402:pending:" <> key]) do
          {:ok, _count} -> :ok
          {:error, reason} -> {:error, reason}
        end
      end

      defp decode(encoded) do
        # `:safe` refuses unknown atoms; entry maps only contain literals
        # this module wrote, so decoding failures indicate store corruption.
        Base.decode64!(encoded) |> :erlang.binary_to_term([:safe])
      end
    end

# `adapter`

```elixir
@type adapter() :: {module(), term()}
```

Adapter tuple accepted by `X402.Facilitator.Engine`.

# `entry`

```elixir
@type entry() :: %{
  transaction: String.t(),
  provenance: :node_acknowledged | :local_hash,
  raw_transaction: binary() | nil
}
```

A recorded pending settlement.

# `get_result`

```elixir
@type get_result() :: {:hit, entry()} | :miss | {:error, term()}
```

Result returned by `get/2`.

# `key`

```elixir
@type key() :: String.t()
```

Key identifying a payment's settlement attempt (a lowercase hex digest).

# `write_result`

```elixir
@type write_result() :: :ok | {:error, term()}
```

Result returned by write/delete operations.

# `delete`

```elixir
@callback delete(store :: term(), key()) :: write_result()
```

Deletes the entry for `key`. Deleting an absent key returns `:ok`.

# `get`

```elixir
@callback get(store :: term(), key()) :: get_result()
```

Reads the entry stored for `key`, or `:miss` when absent or expired.

# `put`

```elixir
@callback put(store :: term(), key(), entry()) :: write_result()
```

Unconditionally stores `entry` for `key`, resetting its TTL.

# `delete`
*since 0.6.0* 

```elixir
@spec delete(adapter(), key()) :: write_result()
```

Deletes `key` through the `{module, store}` adapter.

Adapter crashes never propagate: a raise, throw, or exit from the adapter
is caught and returned as `{:error, {:store_unavailable, reason}}`.

# `get`
*since 0.6.0* 

```elixir
@spec get(adapter(), key()) :: get_result()
```

Reads `key` through the `{module, store}` adapter.

Adapter crashes never propagate: a raise, throw, or exit from the adapter
(a dead, restarting, or overloaded store process, for example) is caught
and returned as `{:error, {:store_unavailable, reason}}`, so callers can
apply their documented store-failure handling —
`X402.Facilitator.Engine.settle/3` treats a failed read as a miss and
falls through to a normal broadcast.

# `put`
*since 0.6.0* 

```elixir
@spec put(adapter(), key(), entry()) :: write_result()
```

Writes `entry` under `key` through the `{module, store}` adapter.

Adapter crashes never propagate: a raise, throw, or exit from the adapter
is caught and returned as `{:error, {:store_unavailable, reason}}` —
`X402.Facilitator.Engine.settle/3` downgrades a failed write to a
terminal response that keeps the broadcast transaction hash.

# `validate_adapter`
*since 0.6.0* 

```elixir
@spec validate_adapter(term()) :: {:ok, adapter()} | {:error, String.t()}
```

Validates a `{module, store}` adapter tuple for use as a NimbleOptions
`{:custom, ...}` validator.

## Examples

    iex> X402.Facilitator.PendingSettlementStore.validate_adapter(
    ...>   {X402.Facilitator.PendingSettlementStore.ETS, MyStore}
    ...> )
    {:ok, {X402.Facilitator.PendingSettlementStore.ETS, MyStore}}

    iex> X402.Facilitator.PendingSettlementStore.validate_adapter(:not_a_store)
    {:error, "expected {module, store} with module implementing " <>
      "X402.Facilitator.PendingSettlementStore"}

---

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