Appearance
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 API → Firm quotes, and Amount bounds and firm quotes).
Submit Intent
POST /intentsSynchronous; 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"
}
}| Field | Type | Required | Description |
|---|---|---|---|
intentId | string | Yes | Caller-supplied unique ID; also used as the API source/reply reference |
clientReference | string | No | Target. 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 |
user | string | Yes | Account 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 |
inputAsset | object | Yes | Asset you send. See AssetDescriptor |
outputAsset | object | Yes | Asset you receive. See AssetDescriptor |
inputAmountMinor | string | Yes | Input 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 |
minOutputAmountMinor | string | Yes | Minimum output you accept, in the output asset's minor unit. Below this → below_minimum_output. In the bounds model this is output.limitAmountMinor |
quoteId | string | No | Reference 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 |
deadline | number | Yes | Unix timestamp expiry |
settlementType | string | Yes | onchain, offchain, or hybrid |
outputRecipient | string | No | Third-party recipient. If set and ≠ user, this is a remittance and beneficiary is required |
beneficiary | object | Conditional | Required for a remittance, and for any fiat output. See Beneficiary |
permit | object | Conditional | Required 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) |
actorType | string | No | retail or institution. Defaults to institution |
side | string | No | buy or sell. Defaults to sell |
metadata | object | No | Compliance / routing metadata. See Metadata |
settlementModeis not part of the request — the solver currently settlesatomic.
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)| Mode | input | output | Meaning |
|---|---|---|---|
| Exact-input (default) | targetAmountMinor == limitAmountMinor (fixed) | limitAmountMinor only (floor) | Fix what you send; solvers compete on output. inputAmountMinor/minOutputAmountMinor are sugar for this. |
| Exact-output | limitAmountMinor 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) | cap | decaying targetAmountMinor → limitAmountMinor | UniswapX-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
quoteIdcome 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 validUntilThe intent never triggers the RFQ; the client obtains the
quoteIdbefore 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/firm→quoteId→POST /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
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | erc20, fiat, or native |
symbol | string | Yes | e.g. USDC, KRW, ETH |
address | string | No | ERC20 contract address (erc20) |
chain | string | No | e.g. ethereum (on-chain assets) |
currency | string | No | ISO 4217 code (fiat), e.g. KRW |
Beneficiary
| Field | Type | Required | Description |
|---|---|---|---|
account | string | Yes | Recipient account number. A pre-provisioned recipient reference (bene_…:dest_…) also goes here — when you send one, the fields below are not needed |
name | string | Yes | Recipient name |
country | string | Conditional | Recipient country, ISO 3166-1 alpha-2 (e.g. US). Required when you send recipient details inline |
payoutMethod | string | Conditional | Payout rail — wire, swift, ach, sepa. Required inline, unless swiftCode is present |
swiftCode | string | No | Recipient bank BIC. Also settles the rail on its own: send it without payoutMethod and the rail is taken as swift |
bank | string | No | Recipient bank name |
address | object | Conditional | Recipient postal address. Required for the wire and swift rails. See BeneficiaryAddress |
travelRuleData | object | No | Travel 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:
countrymissing, or neitherpayoutMethodnorswiftCodepresent →beneficiary_incomplete_for_jit_payoutpayoutMethodiswireorswiftandaddressis 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
| Field | Type | Required | Description |
|---|---|---|---|
line1 | string | Yes | Street address |
city | string | Yes | City |
state | string | Yes | State or province |
postalCode | string | Yes | Postal 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
| Field | Type | Description |
|---|---|---|
senderKycHash | string | Sender KYC reference |
depositReference | string | Deposit reference for the fiat on-ramp leg |
currency | string | Settlement currency hint |
targetStablecoin | string | Stablecoin used for the fiat on-ramp leg |
travelRuleData | object | Travel Rule payload |
beneficiaryAccount | string | Deprecated — use top-level beneficiary.account |
beneficiaryName | string | Deprecated — 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.
| Field | Type | Required | Description |
|---|---|---|---|
token | string | Yes | ERC20 contract address. Must equal inputAsset.address |
amountMinor | string | Yes | Signed spending cap, in the input token's minor unit. Must be ≥ the input amount; settlement pulls at most this much |
spender | string | Yes | The HTLC escrow address for your target environment (provided during onboarding, alongside the NfxPermit2 address) |
nonce | string | Yes | Single-use nonce. Read the next value on-chain: NfxPermit2.nonces(user). One signature = one execution; a reused nonce is rejected |
deadline | number | Yes | Unix 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 |
signature | string | Yes | 65-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.
| Field | Type | Description |
|---|---|---|
intentId | string | Echoes the submitted ID |
status | string | accepted |
planId | string | Settlement plan ID — DVP- + the first 8 chars of intentId, upper-cased |
solverId | string | The solver that accepted the Intent |
inputAmountMinor | string | Present for exact-output intents: computed input required to fill the intent (≤ input.limitAmountMinor). Omitted for exact-input intents |
fees | array | Present 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 |
path | object | The chosen execution plan. See SolvedPath |
SolvedPath
| Field | Type | Description |
|---|---|---|
intentId | string | The Intent this path fulfils |
solverId | string | Solver that produced the path |
outputAmountMinor | string | Final amount delivered to the recipient, in the output asset's minor unit |
surplusAmountMinor | string | Exact-input: outputAmountMinor − minOutputAmountMinor. Exact-output: the input saved, input.limitAmountMinor − inputAmountMinor |
estimatedFillTime | number | Estimated fill time in seconds (≈30 for crypto output, ≈3600 for fiat output) |
steps | array | Execution steps. See Fill steps |
score | number | Solver-internal score (lower is better) |
expiresAt | number | Path 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— forfiatoutput:{ 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"
}reason | Raised when |
|---|---|
beneficiary_required_for_remittance | outputRecipient ≠ user, but beneficiary.account / beneficiary.name is missing |
permit_required | The 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_liquidity | No solver candidate can fill the requested pair |
below_minimum_output | The computed output is less than minOutputAmountMinor / output.limitAmountMinor (exact-input intents) |
above_maximum_input | The input required to deliver the fixed output exceeds input.limitAmountMinor (exact-output intents) |
beneficiary_account_required_for_fiat_output | outputAsset.type is fiat, but no beneficiary account was provided |
beneficiary_incomplete_for_jit_payout | Recipient details are sent inline, but beneficiary.country is missing, or neither beneficiary.payoutMethod nor beneficiary.swiftCode is present. See Beneficiary |
beneficiary_address_required_for_rail | The 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 byorigClOrdId+sessionId. - On-chain: an Intent can be cancelled permissionlessly once its fill deadline has passed.
Notes
- All amounts are decimal strings — no floating-point.
actorTypedefaults toinstitution;sidedefaults tosell.AssetDescriptorhas nodecimalsfield; token decimals are resolved internally.
