Appearance
Quote API
Request a quote — the pair, amount, and side you want — from the Solver Network. The solver searches its liquidity sources, scores the results, and returns either a ranked list of indicative candidates or a firm quote with locked terms. You then execute by submitting an intent (see Executing a quote).
Quoting is a two-step flow. Use POST /v1/quotes for indicative candidates, or POST /v1/quotes/firm when you need a lockable RFQ quote. Execution still happens through the Intent API; there is no separate confirm endpoint and no quote re-fetch — a lapsed quote is simply re-requested.
Base URL
All endpoints are served under the /v1 global prefix:
Production: https://api.ezys.io/v1
Sandbox: https://api.ezys.sh/v1The paths below are shown relative to this base — e.g. POST /quotes is POST /v1/quotes.
Request Quotes
POST /v1/quotesSynchronous; responds 200 OK. The body is a QuoteProposedEvent carrying a candidates array (empty if no liquidity — see Empty candidates). The request context is persisted so a later execution can resolve it by requestId.
The response is returned directly — it is not wrapped in a
{ success, data }envelope.
Request Body
json
{
"requestId": "RFQ-001",
"actorId": "hana-ny",
"actorType": "institution",
"source": "USDC",
"target": "KRW",
"amount": "1000000",
"side": "sell",
"settlementPref": "hybrid"
}| Field | Type | Required | Description |
|---|---|---|---|
requestId | string | Yes | Caller-supplied unique ID; also the context key a later execution uses to resolve this quote |
actorId | string | Yes | Account or ID that owns the request |
source | string | Yes | Asset you send, e.g. USDC |
target | string | Yes | Asset you receive, e.g. KRW |
amount | string | Yes | Amount of source to quote, decimal string |
side | string | Yes | buy or sell |
actorType | string | No | retail or institution. Defaults to institution |
quoteType | string | No | A_retail or B_institution. Defaults to B_institution |
settlementPref | string | No | onchain, offchain, or hybrid. Defaults to hybrid |
maxSlippageBps | number | No | Max slippage you accept, in basis points. Mapped to constraints.maxSlippageBps |
Quotes Response
The response echoes your requestId and returns the scored candidates (top 5 by default, best first).
json
{
"requestId": "RFQ-001",
"timestamp": 1737123456789,
"candidates": [
{
"routeId": "external-lp-sim:LP-A:USDC-KRW",
"ttlSec": 30,
"totalCost": "1445.00000000",
"slippageBps": 5,
"provider": "LP-A",
"hops": ["rfq:LP-A:USDC-KRW"],
"legs": [
{
"from": "USDC",
"to": "KRW",
"venue": "LP:LP-A",
"price": "1445.00000000",
"feeBps": 8,
"capacity": "700000000"
}
],
"metadata": {
"venue": "ExternalLP",
"lpId": "LP-A",
"confidence": 0.94,
"isDynamic": false,
"latencyMs": 320,
"basePrice": 1443.1,
"feesBps": 8,
"liquidityAvailable": 700000000,
"score": 1445.02,
"constraintViolations": []
}
}
]
}| Field | Type | Description |
|---|---|---|
requestId | string | Echoes the submitted ID |
timestamp | number | Response time (Unix ms) |
candidates | array | Scored quote candidates, best first. Empty [] means no liquidity. See Candidate |
Candidate
| Field | Type | Description |
|---|---|---|
routeId | string | Opaque route handle identifying the candidate's route. Its internal shape varies by liquidity source (e.g. external-lp-sim:LP-A:USDC-KRW) — treat it as opaque |
ttlSec | number | Quote validity window, in seconds. Honor it client-side and re-request once it lapses — see Status & lifecycle |
totalCost | string | Total price for the route, decimal string |
slippageBps | number | null | Estimated slippage in basis points. Nullable in the schema, but populated by all current sources |
provider | string | null | Liquidity provider / source ID. Nullable in the schema, but populated by all current sources |
hops | array | Routing path (defaults to []; populated by all current sources) |
legs | array | Per-segment breakdown. See Leg |
metadata | object | Scoring / diagnostic detail — see Metadata |
Leg
| Field | Type | Description |
|---|---|---|
from | string | Segment source asset |
to | string | Segment target asset |
venue | string | null | Where the segment fills, e.g. LP:<lpId>, ExternalLP, AMM-Pool, Orderbook |
price | string | null | Segment price |
feeBps | number | null | Segment fee in basis points |
capacity | string | null | Supply ceiling — the LP's available balance in the target asset. The most this candidate can fill; used to bound split-fill slices |
Metadata
Diagnostic detail attached to each candidate on the HTTP response. Some keys are always computed during scoring; others are source-specific.
| Field | Type | Description |
|---|---|---|
latencyMs | number | Source's quote latency, milliseconds (scoring input) |
basePrice | number | Reference/oracle price before spread and fees |
feesBps | number | Fee applied by the route, basis points |
liquidityAvailable | number | Available liquidity considered during scoring |
score | number | Solver's internal score for ranking (lower is better) |
constraintViolations | array | Constraint checks the candidate failed (e.g. slippage>50); always present, [] when none |
Source-specific keys may also appear — e.g. ExternalLP adds
venue,lpId,lpName,policyBps,confidence,isDynamic; Orderbook addsorderbookDepth. Treatmetadataas an open, source-dependent object.
Empty candidates
A 200 response with candidates: [] means no liquidity source can fill the requested pair right now — a normal outcome. Submit a fresh request (optionally adjusting the pair or amount) to try again.
Executing a quote
Execution converges on the Intent API: express what you want as amount bounds, optionally attach a firm quoteId (see Firm quotes) to accept a locked quote, and submit the intent — see Intent API → Amount bounds and firm quotes.
Status & lifecycle
There is no quote re-fetch endpoint — quotes are short-lived by design. Each candidate carries a ttlSec validity window: honor it client-side and request a fresh quote once it lapses. Execution against an expired quote is rejected, never silently re-priced.
Once execution is submitted (see Executing a quote), it proceeds asynchronously through the settlement flow. Track progress via status reads and webhooks — see the Status API. A denial at the execution stage (e.g. a policy/limit check) surfaces as a rejected state with a machine-readable code — see Error Handling & Retries.
Firm quotes
POST /v1/quotes/firmSynchronous; responds 201 Created. The response is returned directly — it is not wrapped in a { success, data } envelope.
A firm quote is an RFQ quote whose issuing LP commits the rate and itemized fees for the quote's TTL — the LP warehouses the spread risk for that window, the same way a UniswapX exclusive quoter commits to fill at its quoted price. Firm quotes are what make guaranteed, fee-inclusive transfers possible (e.g. "the recipient receives exactly USD 100,000.00 and my total cost is locked"):
- Request a firm quote — the response carries a
quoteId, the locked all-in rate, an itemizedfeesbreakdown (protocol_fee,payout_fee,network_fee— the same taxonomy as the receipt, so quote and receipt reconcile pertype), andvalidUntil. - Attach the
quoteIdto an Intent (see Intent API → Amount bounds and firm quotes). - Within the TTL, settlement enforces delivery at the quoted terms; past
validUntilthe quote lapses and the intent is rejected — never silently re-priced.
Quoted fees are locked charges, not cost estimates. A
deductedfee item is a commitment — the amount charged at settlement is exactly the amount quoted, regardless of what the underlying cost turns out to be; the quoting side warehouses the variance (the same way a UniswapX filler commits an all-in price including gas, or a bank commits an OUR wire fee upfront). Consequently, the receipt'sfees[]must match the locked quote pertype— a mismatch is a contract violation, not a rounding artifact. The only exception isabsorbeditems (e.g.network_feefronted by Ezys): they are informational estimates, charged to no one, and may differ on the receipt where the actual measured value is reported.
Firm Quote Request Body
json
{
"requestId": "RFQ-FIRM-001",
"actorId": "fsp-narae",
"actorType": "institution",
"side": "sell",
"inputAsset": {
"type": "erc20",
"symbol": "USDC",
"chain": "ethereum"
},
"outputAsset": {
"type": "fiat",
"symbol": "USD",
"currency": "USD"
},
"amount": "10000.00",
"amountType": "target"
}| Field | Type | Required | Description |
|---|---|---|---|
requestId | string | Yes | Caller-supplied unique ID for the quote request |
actorId | string | Yes | Account or partner ID that owns the request |
actorType | string | No | retail or institution. Defaults to institution |
side | string | No | buy or sell. Defaults to sell |
inputAsset | object | Yes | Asset you pay. Same AssetDescriptor shape as the Intent API |
outputAsset | object | Yes | Asset the recipient receives. Same AssetDescriptor shape as the Intent API |
amount | string | Yes | Amount to quote. Interpreted according to amountType |
amountType | string | Yes | source means amount is the input you will pay; target means amount is the exact recipient amount to lock |
Use amountType: "target" for destination-fixed payout. In that mode the solver computes the required input by adding locked deducted fees to the target output and rounding the required input up, so the recipient is not short-paid. The returned input is the maximum amount expected for the attached intent; the intent can still set an input.limit cap when accepting the quote.
Firm Quote Response
Amounts in input, output, and fees[] follow the Money & Amounts minor-unit convention.
jsonc
{
"requestId": "RFQ-FIRM-001",
"quoteId": "fq_9ab32c1f", // ← attach this to the Intent to accept
"pair": "USDC/USD",
"allInRate": "1.000000", // locked for the TTL — LP commits
"input": { "amountMinor": "10035000000", "currency": "USDC", "exponent": 6 }, // 10,035.00 USDC required
"output": { "amountMinor": "1000000", "currency": "USD", "exponent": 2 }, // exactly USD 10,000.00
"fees": [ // itemized, separate from principal — same taxonomy as the receipt
{ "type": "protocol_fee", "amountMinor": "1000", "currency": "USD", "exponent": 2, "collection": "deducted" },
{ "type": "payout_fee", "amountMinor": "2500", "currency": "USD", "exponent": 2, "collection": "deducted" },
{ "type": "network_fee", "amountMinor": "150000", "currency": "USDC", "exponent": 6, "collection": "absorbed" }
], // FX spread is NOT a fee item — it is inside allInRate
"validUntil": "2026-07-15T12:00:30Z"
}| Field | Type | Description |
|---|---|---|
requestId | string | Echoes the submitted request ID |
quoteId | string | Opaque, single-use firm quote handle. Attach it to POST /v1/intents to accept the quote |
pair | string | Input/output pair |
allInRate | string | Locked all-in rate, expressed as output per one input. FX spread is inside this rate, not a fee item |
input | object | Required input amount, minor-unit object |
output | object | Locked output amount, minor-unit object |
fees | array | Locked itemized fees, using the same taxonomy as receipts: protocol_fee, payout_fee, network_fee |
validUntil | string | ISO 8601 expiry. The accepting intent must arrive before this time |
Accepting a firm quote
Attach the returned quoteId to POST /v1/intents. A firm quote is single-use: once an intent consumes it, the same quoteId cannot be used again. Pair, amount, and TTL are checked at acceptance; mismatches are rejected rather than silently re-priced.
For destination-fixed payout, the accepting intent should use exact-output bounds:
jsonc
{
"input": { "limit": "10100000000" }, // cap: most you will pay
"output": { "target": "10000.00", "limit": "10000.00" },
"quoteId": "fq_9ab32c1f"
}Without a quoteId, quotes are indicative and intents remain best-effort auction orders.
Notes
- All amounts are decimal strings — no floating-point.
- Defaults:
actorType→institution,quoteType→B_institution,settlementPref→hybrid. routeIdis an opaque handle — do not parse it.
