X402.Facilitator.NonceManager (X402 v0.6.0)

Copy Markdown View Source

Serializes fee-payer transaction nonces for concurrent settlements.

X402.Facilitator.Engine.settle/3 broadcasts an EIP-1559 transaction from the facilitator's fee-payer account. Reading eth_getTransactionCount per settlement races under concurrency: two settles can read the same pending nonce, sign two different payments with it, and the node rejects one even though its EIP-3009 authorization was never used.

This process assigns nonces instead, tracking the full lifecycle:

  • checkout/3 — assigns the next nonce (fetching from the node only on first use per address) and marks it in flight.
  • complete/3 — the transaction reached the node; the nonce is consumed.
  • release/3 — the settlement failed before the node could have seen the transaction. The tail nonce is rolled back so no gap forms; a released middle nonce marks the address for a re-fetch once every in-flight settlement drains, since a gap would stall later transactions at the node.
  • reset/2 — forget the address (re-fetch on next checkout). With settlements still in flight, the reset is deferred until they drain, so an in-flight nonce is never reissued.

Start one manager (state is keyed by address) and pass it to X402.Facilitator.Engine.new/1 via :nonce_manager:

children = [
  {X402.Facilitator.NonceManager, name: MyApp.NonceManager},
  ...
]

Per-node only

Nonce tracking lives on the local node. Running the same fee-payer key on several facilitator nodes still races at the chain level — use one fee payer per node, or coordinate externally.

Summary

Types

A function fetching the current pending nonce from the node.

Server identifier accepted by GenServer.call/3.

Functions

Checks out the next nonce for address and marks it in flight.

Returns a specification to start this module under a supervisor.

Marks nonce as consumed: its transaction reached (or may have reached) the node.

Returns nonce unused: its settlement failed before the node could have seen the transaction.

Forgets the tracked nonce state for address.

Starts a nonce manager.

Types

fetch_fun()

@type fetch_fun() :: (-> {:ok, non_neg_integer()} | {:error, term()})

A function fetching the current pending nonce from the node.

server()

@type server() :: GenServer.server()

Server identifier accepted by GenServer.call/3.

Functions

checkout(server, address, fetch_fun)

(since 0.6.0)
@spec checkout(server(), String.t(), fetch_fun()) ::
  {:ok, non_neg_integer()} | {:error, term()}

Checks out the next nonce for address and marks it in flight.

Runs fetch_fun (inside the manager — first use per address and after a drain-triggered re-fetch) when no nonce is tracked. A fetch error — or a raise, which is caught — is returned as an error and nothing is stored.

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

complete(server, address, nonce)

(since 0.6.0)
@spec complete(server(), String.t(), non_neg_integer()) :: :ok

Marks nonce as consumed: its transaction reached (or may have reached) the node.

release(server, address, nonce)

(since 0.6.0)
@spec release(server(), String.t(), non_neg_integer()) :: :ok

Returns nonce unused: its settlement failed before the node could have seen the transaction.

The tail nonce rolls straight back; releasing a middle nonce (later checkouts still in flight) marks the address for a node re-fetch once the in-flight settlements drain, because the resulting gap would stall later transactions.

reset(server, address)

(since 0.6.0)
@spec reset(server(), String.t()) :: :ok

Forgets the tracked nonce state for address.

With settlements still in flight, the reset is deferred until they drain so an in-flight nonce is never reissued.

start_link(opts \\ [])

(since 0.6.0)
@spec start_link(keyword()) :: GenServer.on_start()

Starts a nonce manager.

Options

  • :name — optional registered name.