Skip to content

Intent API

Submit an Intent — what you want to exchange and the constraints you accept — to the Solver Network. The solver computes an execution path and either accepts it (reserving liquidity and starting settlement) or rejects it with a reason.

Submission is single-step: the response is terminal (accepted or rejected) and there is no separate confirm step — see Status & lifecycle and Cancellation. To lock a price before committing, request a firm quote first and attach its quoteId (see Quote APIFirm quotes, and Amount bounds and firm quotes).

Submit Intent

POST /intents

Synchronous; responds 202 Accepted. The body is an accepted or rejected object. On acceptance the solver reserves balance, derives an HTLC hashlock, and starts the DvP settlement workflow.

Signature required

This endpoint state-changes, so X-API-Key alone is rejected. Send X-Signature, X-Timestamp, X-Nonce and X-Idempotency-Key as well — see Request Signing.

Request Body

json
{
  "intentId": "INT-2F8A3B1C",
  "user": "0xabc...def",
  "inputAsset": {
    "type": "erc20",
    "symbol": "USDC",
    "chain": "ethereum",
    "address": "0xA0b8...eB48"
  },
  "outputAsset": {
    "type": "fiat",
    "symbol": "KRW",
    "currency": "KRW"
  },
  "inputAmountMinor": "1000000000",
  "minOutputAmountMinor": "1400000",
  "deadline": 1737123456789,
  "settlementType": "hybrid",
  "outputRecipient": "kr-bank-account-123",
  "beneficiary": {
    "account": "110-123-456789",
    "name": "Hong Gildong",
    "country": "KR",
    "payoutMethod": "wire",
    "swiftCode": "KOEXKRSE",
    "bank": "KEB Hana",
    "address": {
      "line1": "29 Euljiro",
      "city": "Seoul",
      "state": "Jung-gu",
      "postalCode": "04523"
    }
  },
  "permit": {
    "token": "0xA0b8...eB48",
    "amountMinor": "1000000000",
    "spender": "0x1c9E...F63a",
    "nonce": "0",
    "deadline": 1737209856,
    "signature": "0x9f2c...8b1c"
  }
}
FieldTypeRequiredDescription
intentIdstringYesCaller-supplied unique ID; also used as the API source/reply reference
clientReferencestringNoTarget. Your own correlation key for this transaction (e.g. your payment-pipeline ID). Bound at submission and echoed verbatim on the read surfaces — Status, Webhooks, Receipts, reconciliation. The submission response itself does not echo it; it returns only server-issued values (intentId, planId, solverId, status, quoteId, fees, path). Independent of the X-Idempotency-Key header, which is transport-level retry safety — though the field is part of the request body and therefore covered by its fingerprint
userstringYesAccount or address that owns the Intent. For on-chain inputs this must be the wallet that signed the permit — funds are pulled from this address
inputAssetobjectYesAsset you send. See AssetDescriptor
outputAssetobjectYesAsset you receive. See AssetDescriptor
inputAmountMinorstringYesInput amount you send, in the input asset's minor unit (see Money & Amounts). In the generalized bounds model this is input.targetAmountMinor == input.limitAmountMinor — see Amount bounds and firm quotes
minOutputAmountMinorstringYesMinimum output you accept, in the output asset's minor unit. Below this → below_minimum_output. In the bounds model this is output.limitAmountMinor
quoteIdstringNoReference to a locked firm quote (LP-committed via RFQ). When attached, the quoted rate and itemized fees are guaranteed for the quote's TTL. See Amount bounds and firm quotes
deadlinenumberYesUnix timestamp expiry
settlementTypestringYesonchain, offchain, or hybrid
outputRecipientstringNoThird-party recipient. If set and ≠ user, this is a remittance and beneficiary is required
beneficiaryobjectConditionalRequired for a remittance, and for any fiat output. See Beneficiary
permitobjectConditionalRequired for on-chain (erc20) inputs. A signed NfxPermit2 authorization that lets settlement pull the input amount from user's wallet into the HTLC escrow. Without it the intent is rejected with permit_required. See Permit (on-chain inputs)
actorTypestringNoretail or institution. Defaults to institution
sidestringNobuy or sell. Defaults to sell
metadataobjectNoCompliance / routing metadata. See Metadata

settlementMode is not part of the request — the solver currently settles atomic.

Amount bounds and firm quotes

The generalized bounds form and quoteId support destination-fixed payout and firm quote acceptance. The exact-input shape (inputAmountMinor + minOutputAmountMinor) is sugar for the same bounds model.

The Intent amount model generalizes to two orthogonal axes (the same decomposition UniswapX uses — amounts as decaying ranges that can collapse to a point, and firmness supplied by an RFQ winner who commits to fill):

Axis 1 — amounts are bounds that can collapse to a point. Each side carries { targetAmountMinor, limitAmountMinor }. When targetAmountMinor == limitAmountMinor the amount is fixed (a limit order); when they differ, the amount decays from targetAmountMinor toward limitAmountMinor over the intent's lifetime (an auction range).

jsonc
"input":  { "targetAmountMinor": "...", "limitAmountMinor": "..." },   // limit = the most you will pay (cap)
"output": { "targetAmountMinor": "...", "limitAmountMinor": "..." }    // limit = the least you accept (floor)
ModeinputoutputMeaning
Exact-input (default)targetAmountMinor == limitAmountMinor (fixed)limitAmountMinor only (floor)Fix what you send; solvers compete on output. inputAmountMinor/minOutputAmountMinor are sugar for this.
Exact-outputlimitAmountMinor only (cap)targetAmountMinor == limitAmountMinor (fixed)Recipient receives an exact amount — a bank wire with sender-paid fees (OUR). Solvers compete on the input within your cap.
Range (auction)capdecaying targetAmountMinor → limitAmountMinorUniswapX-style Dutch decay; first profitable solver fills.

Axis 2 — firmness comes from a locked quote, not from the amount shape. Without quoteId, an Intent is best-effort: it competes in the auction and may expire unfilled. With quoteId referencing a firm RFQ quote, the quoting LP has committed the rate and itemized fees for the quote's TTL — the LP warehouses the spread risk (exactly like a UniswapX exclusive quoter) and settlement enforces delivery of the quoted output, so the fill is guaranteed within the TTL.

Where does quoteId come from? The firm flow is quote-first — the two flows enter in a different order:

Best-effort (auction):   submit intent ──▶ solvers compete ──▶ fill or expire
                         (no quoteId anywhere — the auction is the price discovery)

Firm (RFQ-accept):       ① request firm quote (Quote API)          — "what will it cost?"
                         ② receive quoteId + locked rate/fees/TTL  — LP commits
                         ③ submit intent WITH quoteId              — "I accept this quote"
                         ④ guaranteed fill within validUntil

The intent never triggers the RFQ; the client obtains the quoteId before submitting, and the intent that carries it is semantically the acceptance of that quote. This is the same handle-passing shape as the firm flow (POST /quotes/firmquoteIdPOST /intents) — except the handle is LP-committed rather than advisory.

Exact-output, best-effort (no quoteId — auction, may expire):

jsonc
{
  "intentId": "INT-9C1D2E3F",
  "user": "0xabc...def",
  "inputAsset":  { "type": "erc20", "symbol": "USDC", "chain": "ethereum", "address": "0xA0b8...eB48" },
  "outputAsset": { "type": "fiat",  "symbol": "USD",  "currency": "USD" },
  "input":  { "limitAmountMinor": "100305000000" },                    // cap: most you'll pay — 100,305 USDC
  "output": { "targetAmountMinor": "10000000", "limitAmountMinor": "10000000" }, // exactly USD 100,000.00
  "deadline": 1737123456789,
  "settlementType": "hybrid",
  "outputRecipient": "us-bank-account-123",
  "beneficiary": { "account": "...", "name": "Jane Doe", "bank": "..." }
}

Exact-output, firm (locked quote attached — guaranteed within TTL):

jsonc
{
  // ...same as above, plus:
  "quoteId": "fq_9ab32c1f"    // ← from the firm-quote response (Quote API → Firm quotes):
                              //   locked allInRate, required input,
                              //   itemized fees, validUntil 2026-07-15T12:00:30Z
}

Amounts follow the Money & Amounts convention. For exact-output intents the accepted response returns the computed inputAmountMinor; when a quoteId is attached, the fees breakdown is itemized from the locked quote (see Accepted Response). Firm quotes are issued by the Quote API RFQ flow — the fq_9ab32c1f above is the quoteId returned by the firm-quote response example in Quote API → Firm quotes.

AssetDescriptor

FieldTypeRequiredDescription
typestringYeserc20, fiat, or native
symbolstringYese.g. USDC, KRW, ETH
addressstringNoERC20 contract address (erc20)
chainstringNoe.g. ethereum (on-chain assets)
currencystringNoISO 4217 code (fiat), e.g. KRW

Beneficiary

FieldTypeRequiredDescription
accountstringYesRecipient account number. A pre-provisioned recipient reference (bene_…:dest_…) also goes here — when you send one, the fields below are not needed
namestringYesRecipient name
countrystringConditionalRecipient country, ISO 3166-1 alpha-2 (e.g. US). Required when you send recipient details inline
payoutMethodstringConditionalPayout rail — wire, swift, ach, sepa. Required inline, unless swiftCode is present
swiftCodestringNoRecipient bank BIC. Also settles the rail on its own: send it without payoutMethod and the rail is taken as swift
bankstringNoRecipient bank name
addressobjectConditionalRecipient postal address. Required for the wire and swift rails. See BeneficiaryAddress
travelRuleDataobjectNoTravel Rule payload

Three of these are conditionally required — the request schema cannot express it statically because the requirement depends on the rail and on how you supply the recipient. They are checked when the intent is submitted, before any funds move:

  • country missing, or neither payoutMethod nor swiftCode present → beneficiary_incomplete_for_jit_payout
  • payoutMethod is wire or swift and address is incomplete → beneficiary_address_required_for_rail

Sending a pre-provisioned recipient reference in account skips these checks — the recipient is already registered with the payout partner.

BeneficiaryAddress
FieldTypeRequiredDescription
line1stringYesStreet address
citystringYesCity
statestringYesState or province
postalCodestringYesPostal code

All four are required. A partially filled address is rejected the same way a missing one is — the receiving bank requires a complete postal address on wire rails. Make all four mandatory in your recipient-registration form.

Metadata

FieldTypeDescription
senderKycHashstringSender KYC reference
depositReferencestringDeposit reference for the fiat on-ramp leg
currencystringSettlement currency hint
targetStablecoinstringStablecoin used for the fiat on-ramp leg
travelRuleDataobjectTravel Rule payload
beneficiaryAccountstringDeprecated — use top-level beneficiary.account
beneficiaryNamestringDeprecated — use top-level beneficiary.name

Permit (on-chain inputs)

On-chain (erc20) inputs are user-funded: settlement never spends its own inventory on your behalf. Instead, you attach a one-time NfxPermit2 authorization — an EIP-712 signature that allows the escrow to pull the input amount from user's wallet when the settlement workflow locks it. You pay no gas per intent and broadcast no transaction; the platform relays execution.

One-time prerequisite (per wallet, per token): approve the NfxPermit2 contract on the ERC20 you will send — ERC20.approve(nfxPermit2, largeAllowance). This is the only on-chain transaction you ever submit; every intent afterwards needs only a signature.

FieldTypeRequiredDescription
tokenstringYesERC20 contract address. Must equal inputAsset.address
amountMinorstringYesSigned spending cap, in the input token's minor unit. Must be ≥ the input amount; settlement pulls at most this much
spenderstringYesThe HTLC escrow address for your target environment (provided during onboarding, alongside the NfxPermit2 address)
noncestringYesSingle-use nonce. Read the next value on-chain: NfxPermit2.nonces(user). One signature = one execution; a reused nonce is rejected
deadlinenumberYesUnix timestamp in seconds (unlike the intent-level deadline, which is milliseconds). Settlement locks asynchronously after the 202 — leave generous margin (hours, not minutes), or the lock fails with permit deadline has already expired
signaturestringYes65-byte hex EIP-712 signature. Must be signed by the user wallet — any other signer fails on-chain recovery
Signing

Sign the PermitSingle struct with eth_signTypedData_v4 (natively supported by MetaMask, Rabby, Ledger, WalletConnect wallets — no plugin needed) or any server-side EIP-712 signer:

ts
// viem — works in the browser (walletClient from a connected wallet)
// and server-side (privateKeyToAccount / KMS signer)
const signature = await walletClient.signTypedData({
  account: user,
  domain: {
    name: 'NfxPermit2',
    version: '1',
    chainId,                       // target chain
    verifyingContract: nfxPermit2, // NfxPermit2 address (per environment)
  },
  types: {
    PermitSingle: [
      { name: 'token',    type: 'address' },
      { name: 'amount',   type: 'uint256' },
      { name: 'spender',  type: 'address' },
      { name: 'nonce',    type: 'uint256' },
      { name: 'deadline', type: 'uint256' },
    ],
  },
  primaryType: 'PermitSingle',
  message: { token, amount, spender, nonce, deadline },
})

The REST field permit.amountMinor is mapped into the EIP-712 PermitSingle.amount value. The signature is bound to the spender (only the escrow can consume it), capped by that amount, expires at deadline, and its nonce is burned on use — a leaked signature cannot redirect funds elsewhere, and an unused one simply expires.

Malformed permits are rejected with 400 before submission is accepted (e.g. permit.token must match inputAsset.address, permit.signature must be a 65-byte hex value, permit.amountMinor must cover the locked amount). A structurally valid but missing permit on an erc20 input is rejected with permit_required.

Accepted Response

json
{
  "intentId": "INT-2F8A3B1C",
  "status": "accepted",
  "planId": "DVP-INT-2F8A",
  "solverId": "solver-1",
  "path": {
    "intentId": "INT-2F8A3B1C",
    "solverId": "solver-1",
    "outputAmountMinor": "1424500",
    "surplusAmountMinor": "24500",
    "estimatedFillTime": 3600,
    "steps": [
      {
        "type": "fiat_gateway",
        "asset": "KRW",
        "amountMinor": "1424500",
        "provider": "fiat-gateway",
        "currency": "KRW",
        "beneficiaryAccount": "110-123-456789"
      }
    ],
    "score": 0,
    "expiresAt": 1737123756789
  }
}

The response is returned directly — it is not wrapped in a { success, data } envelope.

FieldTypeDescription
intentIdstringEchoes the submitted ID
statusstringaccepted
planIdstringSettlement plan ID — DVP- + the first 8 chars of intentId, upper-cased
solverIdstringThe solver that accepted the Intent
inputAmountMinorstringPresent for exact-output intents: computed input required to fill the intent (≤ input.limitAmountMinor). Omitted for exact-input intents
feesarrayPresent when the intent references a firm quoteId. Itemized from the locked quote, with the same taxonomy as the receipt: [{ type, amountMinor, currency, exponent, collection }] (amounts follow the Money & Amounts convention), type = protocol_fee | payout_fee | network_fee. Quote-time values are the locked charge; the receipt reports what was actually charged — reconcile the two per type. FX spread is not a fee item (it is a component of the all-in rate). Kept separate from principal, not baked into the amounts above
pathobjectThe chosen execution plan. See SolvedPath

SolvedPath

FieldTypeDescription
intentIdstringThe Intent this path fulfils
solverIdstringSolver that produced the path
outputAmountMinorstringFinal amount delivered to the recipient, in the output asset's minor unit
surplusAmountMinorstringExact-input: outputAmountMinor − minOutputAmountMinor. Exact-output: the input saved, input.limitAmountMinor − inputAmountMinor
estimatedFillTimenumberEstimated fill time in seconds (≈30 for crypto output, ≈3600 for fiat output)
stepsarrayExecution steps. See Fill steps
scorenumberSolver-internal score (lower is better)
expiresAtnumberPath validity deadline (Unix ms; ≈ now + 5 min)

Fill steps

Each path.steps[] entry is one of the following types: reserve, dex_swap, cex_otc, bridge, cow_match, fiat_gateway. Every step carries type, asset, amountMinor, and optional chain; type-specific fields are added per kind.

The solver currently emits one step per Intent:

  • fiat_gateway — for fiat output: { type, asset, amountMinor, provider, currency, beneficiaryAccount }
  • reserve — for crypto output: { type, asset, amountMinor, chain, source } (filled from the solver's own reserve)

Rejected Response

json
{
  "intentId": "INT-2F8A3B1C",
  "status": "rejected",
  "reason": "no_liquidity"
}
reasonRaised when
beneficiary_required_for_remittanceoutputRecipientuser, but beneficiary.account / beneficiary.name is missing
permit_requiredThe input is an on-chain (erc20) asset but no permit was attached. On-chain inputs are user-funded via a signed NfxPermit2 authorization — see Permit (on-chain inputs)
no_liquidityNo solver candidate can fill the requested pair
below_minimum_outputThe computed output is less than minOutputAmountMinor / output.limitAmountMinor (exact-input intents)
above_maximum_inputThe input required to deliver the fixed output exceeds input.limitAmountMinor (exact-output intents)
beneficiary_account_required_for_fiat_outputoutputAsset.type is fiat, but no beneficiary account was provided
beneficiary_incomplete_for_jit_payoutRecipient details are sent inline, but beneficiary.country is missing, or neither beneficiary.payoutMethod nor beneficiary.swiftCode is present. See Beneficiary
beneficiary_address_required_for_railThe rail is wire or swift and beneficiary.address is missing or incomplete — all four of line1, city, state, postalCode are required. See BeneficiaryAddress

Validation runs in the order above; the first failure is returned.

Infrastructure errors

Infrastructure failures (e.g. misconfiguration, or the settlement workflow failing to start) propagate as 5xx. When settlement startup fails after a balance reservation, the reservation is released before the error surfaces.

Status & lifecycle

This endpoint is terminal at submission time — it returns only accepted or rejected. After acceptance, settlement proceeds asynchronously through the DvP/HTLC workflow; track progress via the Status API (GET /executions/{reference}) and status webhooks. The authoritative on-chain state is tracked by the Intent settler contract: Created → Filled → Settled, or Cancelled.

Cancellation

There is no DELETE on this endpoint. Cancellation is initiated through other channels:

  • FIX (institutional): an OrderCancelRequest (35=F) cancels the intent, correlated by origClOrdId + sessionId.
  • On-chain: an Intent can be cancelled permissionlessly once its fill deadline has passed.

Notes

  • All amounts are decimal strings — no floating-point.
  • actorType defaults to institution; side defaults to sell.
  • AssetDescriptor has no decimals field; token decimals are resolved internally.