Ledger-based expiration
A Stellar authorization entry is valid only until a specific ledger,signatureExpirationLedger. There are no wall-clock timestamps in the auth. The buyer’s client derives the ledger from the seller’s maxTimeoutSeconds:
maxTimeoutSeconds of 60 and roughly 5-second ledgers, that is about 12 ledgers, or about a minute. After that ledger the authorization is dead and the buyer must sign a fresh one.
The facilitator enforces the bound in both directions:
- Lower bound. An entry whose expiration is already in the past is rejected before submission.
- Upper bound. An entry whose expiration is too far in the future is also rejected.
@x402/stellarallows a small tolerance (maxLedger + 2) to absorb RPC skew, meaning a few seconds where the facilitator’s view of the current ledger lags the network. Rail402 mirrors that tolerance forexactand reimplements it forupto(ceil(seconds/5) + 2), rejecting an over-far entry withinvalid_upto_stellar_payload_expiration_too_far.
The upper bound is not cosmetic. For
upto, the single-use nonce record is stored with a TTL derived from the same window. If an authorization could expire after its nonce record, the record that makes it single-use would vanish while the authorization was still live, so the contract refuses any expiration beyond current + NONCE_TTL_LEDGERS. See the contract guarantees.The verify-to-settle race
Verify and settle are two separate calls. The 60-second window can elapse between them, for example while the seller runs the metered work or the network is briefly slow, and the authorization that was valid at verify is expired at settle. That is a normal outcome, not an error condition, and it must be reported as such. Rail402 detects it at settle by matching the Soroban host phrasesignature has expired and returns a non-retryable code:
Non-retryable is the correct flag: retrying with the same expired authorization will fail forever. The right recovery is for the buyer to sign a fresh authorization, which the reason tells it to do. A facilitator that returned a retryable error here, or crashed, would trap an agent in a retry loop.
Replay resistance
An authorization is single-use, and Rail402 enforces it at two layers. Forexact, the SAC’s own nonce mechanism rejects a re-submitted authorization at the Soroban auth layer: the host raises Error(Auth, ExistingValue) (“nonce already exists”). Rail402’s classifier matches both that structured token and the human phrasing, and returns invalid_exact_stellar_payload_authorization_replayed, non-retryable.
For upto, replay is refused twice over. The Soroban auth layer rejects the reused nonce exactly as above, and the upto contract independently records each nonce in temporary() storage and checks-then-sets it before any transfer, so even a hypothetical auth-layer bypass meets a contract that has already consumed the nonce. A test replays a captured payload and confirms the second attempt fails.
Front-running resistance
A front-runner who observes a signed authorization in flight cannot profit from it, because the authorization binds every value that matters. The auth entry commits to the exactfrom, to, amount, and asset (and, for upto, the ceiling, recipient, and settlement contract). A third party cannot redirect the transfer to a different recipient, inflate the amount, or change the asset, because any such change invalidates the signature. And because the authorization is single-use, a front-runner cannot resubmit the identical transfer to a different block position for gain; the second submission is a replay and is refused.
The facilitator submits the authorization exactly as signed. There is no code path in which it can alter a bound value, which is the same property that makes it non-custodial.
Where it lives
Next steps
The upto scheme
The contract that enforces single-use on chain.
The settlement path
Where auth-entry validation happens.
Wire-level conformance
The replay and tamper negative tests.
Error registry
The full set of settlement rejection codes.