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
@type fetch_fun() :: (-> {:ok, non_neg_integer()} | {:error, term()})
A function fetching the current pending nonce from the node.
@type server() :: GenServer.server()
Server identifier accepted by GenServer.call/3.
Functions
@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.
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec complete(server(), String.t(), non_neg_integer()) :: :ok
Marks nonce as consumed: its transaction reached (or may have reached)
the node.
@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.
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.
@spec start_link(keyword()) :: GenServer.on_start()
Starts a nonce manager.
Options
:name— optional registered name.