Why two signatures
This is the part that is specific to Flare, and the part most x402 implementations get to skip.
An EIP-3009 authorisation - what USDC uses - names its
recipient. The signature itself says move X to this address.
An EIP-2612 permit, which is what FXRP implements,
does not. It commits only to
(owner, spender, value, nonce, deadline): it grants an
allowance and says nothing about where the money then goes.
settle() is permissionless by design, because any relayer
should be able to carry a payment. That turns the missing recipient
from an omission into a hole: anyone who observed a permit signature -
in the mempool, or on the x402 HTTP path it necessarily travels - could
call settle() naming themselves as payee.
The permit
- owner
- spender
- value
- nonce
- deadline
Authorises an allowance. Says nothing about the destination.
The PaymentIntent
- invoiceId
- payer
- payee
- amount
- deadline
Authorises the destination. Every field a relayer supplies as calldata appears here, so none can be substituted.
The permit authorises the allowance; the intent authorises the destination. Neither is sufficient alone - which is why a relayer can choose when a payment lands, but nothing about what it is.
Step by step
-
The endpoint answers 402
The price is quoted in US dollars, then converted to FXRP at Flare's FTSO rate. The invoice carries an ID that makes the payment single-use.
-
The agent signs twice, offchain
An EIP-2612 permit over FXRP's domain, and a PaymentIntent over the facilitator's. No transaction, no gas, no chain interaction at all - just two signatures.
-
A relayer submits
settle()The relayer pays the fee. It cannot redirect the payment, inflate the amount, or reuse the invoice, because all three are covered by the intent digest.
-
The contract measures what arrived
FAssets parameters are asset-manager controlled and may levy a transfer fee, so
settle()reads the payee's balance before and after and reverts withUnderdeliveredif the payee got less than the invoice. A rail that silently underpays is a broken one.
What a front-runner can and cannot do
A permit signature is visible the moment it travels. Someone can lift
it from the mempool and submit it straight to FXRP, which consumes the
payer's nonce. A bare permit call inside
settle() would then revert - griefing every payment on the
rail for the price of gas.
So the permit call is wrapped in try/catch. Swallowing
that revert is only safe because the intent already fixed the
destination: a griefer who replays the permit merely moves the
allowance into place and gains nothing. The allowance is then checked
explicitly, so a genuinely missing one still reports itself rather than
failing opaquely inside the token.
This is tested rather than argued - see what is verified.
What comes next
EIP-2612 nonces are sequential per payer, so an agent paying two endpoints concurrently has the second permit fail on a consumed nonce. EIP-3009's random nonces avoid this and FXRP does not implement EIP-3009, so payments are serialised per payer today.
Everything else on this page is settled. settled is keyed
on (invoiceId, payer), so a stranger who lifts an invoice
id off the HTTP path lands in their own slot and leaves the real one
payable; and a zero-amount intent, which moved nothing while still
occupying an invoice and costing a relayer its gas, is refused. Both
are in the deployed contract and covered by the fork test.