Skip to main content

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.
Signer + permissions. Value operations are signed by one of your registered signer wallets — your primary by default. On the AMM you can pass source_wallet on accept to use another registered signer; RFQ always uses your primary signer. If the account is not permitted for the action (identity verification required, wallet suspended, or over the unverified daily cap), the call is refused with 403 upstream_error. Register a signer first via POST /wallets.

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 a permit2_witness payload:
relay.witnessTypeString is exactly:
Sign it as a Permit2 PermitWitnessTransferFrom:
Then 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 its tx_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 when signing.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:
Then call 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 by quote.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/orders returns 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 gets 404.
  • Webhooks fx.order.settled, fx.order.failed and fx.order.refunded fire when BlankFX observes those states. expired sends no webhook. See Webhooks.