FX trading
FX runs across two rails behind one flow: the AMM (indicative pool quotes, gasless Permit2 relay) and RFQ (firm market-maker quotes, on-chain escrow). Both are non-custodial — you sign the value operation yourself.Supported assets depend on the network. Call
GET /currencies for the live set. On Sepolia (testnet) this includes USDC, USDT and EURC. An unknown symbol or an unquotable pair returns 502 no_quotes from POST /fx/quotes; the same asset on both sides returns 400 invalid_request. The examples below use USDC → EURC.The flow
POST /fx/quotes takes { "sell_asset": "USDC", "buy_asset": "EURC", "amount": "100" } and returns up to one quote per rail. Each quote has route ("amm" or "rfq") and firm (true for RFQ, false for AMM). RFQ quotes are offered only for cross-currency pairs (for example USD → EUR); a same-currency pair such as USDC → USDT quotes on the AMM only.
Order states
settled, failed, expired and refunded are final. RFQ funded is brief: as soon as your funding is verified, BlankFX hands the order to the market maker and it moves to settling. For RFQ, GET /fx/orders/{id} re-checks the market maker’s status on every call, so poll it until the order is final.
AMM — gasless relay
The accept response returns apermit2_witness payload:
relay.witnessTypeString is exactly:
PermitWitnessTransferFrom:
POST /fx/orders/{id}/submit { "signature": "0x…" }. The signer must have approved Permit2 for tokenIn once (a standard ERC-20 approve to the Permit2 contract). The relayer only broadcasts your signed order — it cannot move funds without your signature, and the contract rejects a signature that doesn’t match the exact order.
RFQ — self-broadcast escrow funding
RFQ differs at the submit step: you broadcast the escrow-funding transaction from your wallet (it costs you gas), then post itstx_hash. The accept response carries everything you need:
funding.quote is the market maker’s signed firm quote, and funding.signature is the market maker’s signature over it. quote.taker and quote.beneficiary are your primary signer wallet; quote.minOut is the exact amount of tokenOut you receive.
1
Approve the escrow
From
quote.taker, ERC-20 approve the escrow (funding.escrow) for at least quote.amountIn of quote.tokenIn. (Sepolia USDT refuses a change from one non-zero allowance to another; set it to 0 first.)2
Fund the escrow yourself
From the same wallet, call
escrowQuote(funding.quote, funding.signature) on funding.escrow before quote.deadline. The escrow locks your tokenIn and emits QuoteEscrowed.3
Submit the tx hash
POST /fx/orders/{id}/submit { "tx_hash": "0x…" }. BlankFX checks the transaction contains a QuoteEscrowed event matching this quote, marks the order funded, and asks the market maker to fill. The market maker then calls fillEscrowedQuote, which pays quote.minOut of tokenOut to the beneficiary and releases your escrowed tokenIn to the market maker in the same transaction.Permit2 funding variant
Some environments also offer funding through Permit2. Use it only whensigning.funding.permit2Funding.enabled is true; relay.fundingMethod stays escrowQuote either way, so don’t use it as the signal. When enabled, permit2Funding carries permit2, witnessTypeString and a funding struct. Sign a Permit2 PermitWitnessTransferFrom with domain { name: "Permit2", chainId, verifyingContract: permit2Funding.permit2 }, permitted = { token: quote.tokenIn, amount: quote.amountIn }, spender = the escrow, nonce / deadline = permit2Funding.funding.nonce / .deadline, and witness type RfqEscrowFunding:
escrowQuoteWithPermit2(funding.quote, funding.signature, permit2Funding.funding, permit2Signature) and submit the tx_hash as above. The field order must match the witness type string exactly, or the escrow rejects the signature.
If the market maker never fills
Nothing refunds automatically. If an order you funded is not filled byquote.deadline, call refundExpiredQuote(quoteHash) on the escrow from quote.taker after the deadline has passed. Use funding.quoteHash (or hashQuote(funding.quote) on the escrow if it is absent). Your locked tokenIn comes back in full; the escrow takes no fee. Refunds work even while the escrow is paused.
Reading orders
GET /fx/ordersreturns up to 50 AMM and 50 RFQ orders, merged and sorted newest first. There is no pagination.GET /fx/orders/{id}returns one order. Orders belong to the account that created them; another account gets404.- Webhooks
fx.order.settled,fx.order.failedandfx.order.refundedfire when BlankFX observes those states.expiredsends no webhook. See Webhooks.