openapi: 3.0.3 info: title: Lux Trading API description: | One surface for every market a Lux interface shows. A market on a Lux chain is quoted from our own contracts. A market on any other chain is quoted from the pools Uniswap deployed there, read directly over that chain's JSON-RPC — no vendor API, no upstream account, nothing that stops working when somebody says no. The response is the same shape either way, because a client should not have to know which arm answered it. Every path lives under /v1/trade. api.lux.cloud carries the chain at /v1/chain, the C-chain at the root and other services besides, so this is one namespace with one owner rather than a handful of generic names on a shared host. Reads are public and unauthenticated. Nothing here signs or sends a transaction: the swap, approval and position paths return unsigned calldata for a wallet to sign. version: 3.0.0 contact: name: Lux Industries url: https://lux.exchange servers: - url: https://api.lux.cloud description: Production - url: http://localhost:8080 description: A local gateway tags: - name: venues - name: tokens - name: quote - name: swap - name: allowance paths: /v1/trade/venues: get: operationId: venues tags: [venues] summary: The chains this deployment can price, and what it reads them from description: | Ask this first. A venue is a contract on a chain, so the answer is grouped by chain: `native` marks a chain we settle ourselves through the Lux precompiles, and the rest are read from their own pools. Without this a client discovers a chain is unquotable by quoting on it and reading an error. parameters: - name: chainId in: query required: false schema: { type: integer, format: int64 } description: Restrict the answer to one chain. responses: '200': description: One entry per chain, in ascending chain id. content: application/json: schema: allOf: - $ref: '#/components/schemas/Envelope' - type: object properties: data: type: array items: { $ref: '#/components/schemas/ChainVenues' } '404': description: No venue here reads that chain. content: application/json: schema: { $ref: '#/components/schemas/Envelope' } /v1/trade/tokens: get: operationId: tokens tags: [tokens] summary: The tokens this deployment can trade description: | The list travels with the binary — nine hundred and thirty-five tokens over the seven chains this gateway prices — so this answers with no upstream, no key and no round trip. Every chain but ours has no indexer behind it, and an interface reading one drew nothing at all on Ethereum. Each entry is that token's identity as its own contract states it: address, symbol, name and the decimals an amount is read with. There is no logo. Every public token list gives logos as URLs on hosts belonging to somebody else, fetched by every viewer's browser on every market screen; marks come from cdn.lux.cloud, and a token with no mark there draws its own letters. Rows come back in the order they are listed: the chain's own token, then the dollar it is priced in, then the rest by symbol. There is no volume on any chain but ours, so there is nothing to rank by. parameters: - name: chainId in: query required: false schema: { type: integer, format: int64 } description: One chain. Omitted, every chain this deployment reads. - name: q in: query required: false schema: { type: string } description: | Narrow to what was typed, nearest first: a whole address, then an exact symbol, then a symbol or name starting with it, then either containing it. An address matches whole and never by fragment — a partial address is a typo, and offering a token that shares a prefix with a mistyped one is how funds reach the wrong contract. - name: limit in: query required: false schema: { type: integer, maximum: 1000 } description: | A ceiling, not a page size. The largest chain here lists 403 and every chain together lists 935, so the default is everything. responses: '200': description: The tokens, in listing order. content: application/json: schema: allOf: - $ref: '#/components/schemas/Envelope' - type: object properties: data: type: array items: { $ref: '#/components/schemas/Token' } '400': $ref: '#/components/responses/BadRequest' '404': description: No venue here reads that chain. content: application/json: schema: { $ref: '#/components/schemas/Envelope' } /v1/trade/price: get: operationId: price tags: [tokens] summary: What one token, or fifty, are worth in dollars description: | There is no oracle behind this and no vendor. The number is a reading of the same pools /v1/trade/quote reads, and it is the same number: spend some dollars of the chain's dollar token, see how much comes back, and put the pool's fee back into the answer — a pool charges its tier on the way through, so what you receive is the mid price less that tier. The row on a market screen and the amount in the swap panel therefore agree. Dollars rather than one whole token, because one whole token is a fifty-cent trade in one market and a hundred-thousand-dollar trade in the next, and the second one moves the pool it is measuring. A token with no dollar pool — on Ethereum, most of them — is read against the chain's wrapped native token instead, whose own price is read against the dollar. Every route is read and the cheapest wins: a price is what somebody actually receives, and the route handing over the most tokens is nearest the middle of the market. **The pools are asked twice, at ten dollars and at a hundred, and a price that does not survive ten times the size is not returned at all.** A pool with almost nothing left in it still answers, and the implied price of a rounding error comes out in the millions. Over the fifty tokens Ethereum lists first, a single reading put half of them more than one percent off an independent source and read one token worth $3.44 at $889,434,176; with both readings, every price returned agrees with that source except where a real pool here is genuinely off the wider market, which is this venue's price and what a swap here would pay. A reading stands for a minute, and `asOf` says when it was taken. A token no pool here holds — or holds too thinly to price — comes back WITHOUT `priceUSD` rather than with a zero: a screen can draw a dash, and it cannot un-draw a wrong number. parameters: - name: chainId in: query required: true schema: { type: integer, format: int64 } - name: token in: query required: true explode: true schema: type: array items: { type: string } description: | Repeated or comma-separated, up to fifty, and answered in the order asked. Fifty because each one is a fan-out of eth_calls against a public endpoint; a screen wanting more asks again, and by then most of them are already read. Fifty cold tokens take about nine seconds and the same fifty again take milliseconds. responses: '200': description: One entry per token asked, in that order. content: application/json: schema: allOf: - $ref: '#/components/schemas/Envelope' - type: object properties: data: type: array items: { $ref: '#/components/schemas/Price' } '400': $ref: '#/components/responses/BadRequest' '404': description: No venue here reads that chain, or it lists no such token. content: application/json: schema: { $ref: '#/components/schemas/Envelope' } '502': description: | The chain could not be read at all — every token asked for failed to reach it. That is a fact about an endpoint and not about the pools, and a page of dashes would say the wrong one. content: application/json: schema: { $ref: '#/components/schemas/Envelope' } '504': description: | Nothing could be read in time. This deployment is busy rather than the chain being unreachable, which is a different place to look: ask for fewer tokens, or ask again in a moment. content: application/json: schema: { $ref: '#/components/schemas/Envelope' } /v1/trade/quote: post: operationId: quote tags: [quote] summary: The best price across every venue that holds the pair description: | Our own chains are asked first, because they are the venue — an upstream opinion about a market we settle ourselves is a second opinion. A venue holding no liquidity returns nothing rather than an error, so the next source is asked in turn. `providerName` and `route[].poolType` name the venue that answered. A quote is a reading of a pool at a moment and is cached for seconds. requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/QuoteRequest' } responses: '200': description: The best price, and where it came from. content: application/json: schema: allOf: - $ref: '#/components/schemas/Envelope' - type: object properties: data: { $ref: '#/components/schemas/Quote' } '400': { $ref: '#/components/responses/BadRequest' } '404': description: | No venue here holds this pair. On a chain this deployment reads, that is a fact about the pools and not about a missing upstream account. content: application/json: schema: { $ref: '#/components/schemas/Envelope' } /v1/trade/quotes: post: operationId: quotes tags: [quote] summary: Every venue's price for the pair, best first description: | The same question as /v1/trade/quote without the last step, from the same source, so the head of this list is what that path returns. requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/QuoteRequest' } responses: '200': description: Every venue that holds the pair, in descending amountOut. content: application/json: schema: allOf: - $ref: '#/components/schemas/Envelope' - type: object properties: data: type: array items: { $ref: '#/components/schemas/Quote' } '400': { $ref: '#/components/responses/BadRequest' } '404': description: No venue here holds this pair. content: application/json: schema: { $ref: '#/components/schemas/Envelope' } /v1/trade/swap: post: operationId: swap tags: [swap] summary: An unsigned transaction for the best route description: | Quotes the pair, then asks the venue that gave that price to build the transaction — the same contract, so the price and the call cannot come from two different places. Nothing here holds a key or sends anything. `amountOutMinimum` is derived from the quote and `slippage`: a router handed the quoted amount as its floor reverts on any movement at all, including the movement the caller's own trade causes. Zero or nonsense means half a percent. A venue that prices a market it cannot settle answers 404 naming itself, rather than calldata for some other contract. requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/SwapRequest' } responses: '200': description: An unsigned transaction. content: application/json: schema: allOf: - $ref: '#/components/schemas/Envelope' - type: object properties: data: { $ref: '#/components/schemas/UnsignedTx' } '400': { $ref: '#/components/responses/BadRequest' } /v1/trade/approval/check: post: operationId: checkApproval tags: [allowance] summary: What the spender may already move description: | Reads `allowance(owner, spender)` on the token, over the same endpoint the pool is quoted from. `approveTx` is returned only when an approval is actually needed — handed back beside "you already approved" it is a transaction somebody sends, and the user pays gas for a state change that changes nothing. requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/AllowanceRequest' } responses: '200': description: The allowance the chain reports, and whether it covers the amount. content: application/json: schema: allOf: - $ref: '#/components/schemas/Envelope' - type: object properties: data: { $ref: '#/components/schemas/Approval' } '400': { $ref: '#/components/responses/BadRequest' } '502': description: | The chain could not be read, or this deployment does not read it. Never a guessed number. content: application/json: schema: { $ref: '#/components/schemas/Envelope' } /v1/trade/approval/build: post: operationId: buildApproval tags: [allowance] summary: An unsigned ERC-20 approve requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/AllowanceRequest' } responses: '200': description: An unsigned transaction. content: application/json: schema: allOf: - $ref: '#/components/schemas/Envelope' - type: object properties: data: { $ref: '#/components/schemas/UnsignedTx' } '400': { $ref: '#/components/responses/BadRequest' } /v1/trade/permit2/check: post: operationId: checkPermit2 tags: [allowance] summary: Whether a live Permit2 grant covers the amount description: | Permit2 answers `(amount, expiration, nonce)`, and the expiration is half the answer: a grant that has run out is not a grant. A `signatureRequest` comes back only when one is needed. requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/Permit2Request' } responses: '200': description: The Permit2 allowance, and whether it is live and sufficient. content: application/json: schema: allOf: - $ref: '#/components/schemas/Envelope' - type: object properties: data: { $ref: '#/components/schemas/Permit2' } '400': { $ref: '#/components/responses/BadRequest' } '502': description: The chain could not be read, or this deployment does not read it. content: application/json: schema: { $ref: '#/components/schemas/Envelope' } /v1/trade/permit2/build: post: operationId: buildPermit2 tags: [allowance] summary: A Permit2 grant, as a transaction or as typed data to sign parameters: - name: mode in: query schema: { type: string, enum: [tx, sig], default: tx } description: | `tx` returns an unsigned Permit2 approve; `sig` returns the EIP-712 PermitSingle for the wallet to sign instead. requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/Permit2Request' } responses: '200': description: An unsigned transaction, or an EIP-712 signature request. content: application/json: schema: { $ref: '#/components/schemas/Envelope' } '400': { $ref: '#/components/responses/BadRequest' } components: responses: BadRequest: description: The request could not be read as asked. content: application/json: schema: { $ref: '#/components/schemas/Envelope' } schemas: Envelope: type: object description: Every answer carries the same envelope. required: [success] properties: success: { type: boolean } data: {} error: type: string description: Present only when success is false. requestId: { type: string } Token: type: object required: [address, chainId] properties: address: { type: string } chainId: { type: integer, format: int64 } decimals: { type: integer } symbol: { type: string } name: { type: string } Price: type: object required: [chainId, address, symbol, name, decimals, asOf] properties: chainId: { type: integer, format: int64 } address: { type: string } symbol: { type: string } name: { type: string } decimals: { type: integer } priceUSD: type: number format: double description: | ABSENT, not zero, when no pool this gateway reads holds the token against either numéraire. venue: type: string description: | The arm that answered. Absent with the price, and absent for the chain's own dollar, which nothing had to read. via: type: string description: | The numéraire the reading was taken against — the chain's dollar, or its wrapped native token. Absent with the price. asOf: type: string format: date-time description: When the pools were read. A reading stands for a minute. Amount: type: object required: [token, amount] properties: token: { $ref: '#/components/schemas/Token' } amount: type: string description: | A whole number of the token's smallest unit, as a decimal string. Never a float — 1e18 does not survive a double. ChainVenues: type: object required: [chainId, native, venues] properties: chainId: { type: integer, format: int64 } native: type: boolean description: True where the chain is settled here through the Lux precompiles. venues: type: array items: { $ref: '#/components/schemas/Venue' } Venue: type: object required: [name, status, executable, type] properties: name: type: string description: | `v4_native` is the Lux precompile arm; `uniswap_v2` and `uniswap_v3` are that chain's own pools, read over its JSON-RPC. example: uniswap_v3 status: { type: string, example: active } executable: type: boolean description: Whether this venue can produce a transaction, as opposed to a price alone. type: { type: string, enum: [on_chain, off_chain] } Hop: type: object properties: poolAddress: { type: string } poolType: type: string description: The venue that holds this hop, by the name it calls itself. example: uniswap_v3 tokenIn: { $ref: '#/components/schemas/Token' } tokenOut: { $ref: '#/components/schemas/Token' } fee: type: integer description: | The pool's fee tier, in hundredths of a basis point. A V3 quote is a reading of ONE pool — the tiers are asked in turn and the best kept — so a swap that does not name this same tier executes against a different pool at a different price. example: 3000 QuoteRequest: type: object required: [tokenIn, tokenOut, chainId, amount] properties: tokenIn: type: string description: Contract address of the token being sold. tokenOut: type: string description: Contract address of the token being bought. chainId: { type: integer, format: int64 } amount: type: string description: A whole number of the smallest unit of tokenIn, or of tokenOut when isExactIn is false. isExactIn: type: boolean default: false description: | True prices a known input. The V2 and V3 arms read the pools with the quoters those protocols published, which answer exact input only; an exact-output question on a chain read that way returns no venue rather than a made-up number. slippage: type: number format: double description: Tolerance in percent, used when this quote becomes a swap. Quote: type: object required: [tokenIn, tokenOut, route, providerName] properties: tokenIn: { $ref: '#/components/schemas/Amount' } tokenOut: { $ref: '#/components/schemas/Amount' } route: type: array items: { $ref: '#/components/schemas/Hop' } priceImpact: { type: number, format: double } gasEstimate: { type: string } quoteId: { type: string } expiresAt: { type: string, format: date-time } providerName: type: string description: The venue that answered. It names itself; nothing renames it. SwapRequest: type: object required: [tokenIn, tokenOut, amount, recipient] properties: tokenIn: { type: string } tokenOut: { type: string } chainId: { type: integer, format: int64 } amount: { type: string } isExactIn: { type: boolean } recipient: type: string description: Who receives tokenOut. slippage: { type: number, format: double } deadline: type: integer format: int64 description: | Unix seconds after which the transaction must not execute. Twenty minutes out when unset. `amountOutMinimum` bounds the price; this bounds the time — a signed swap that sits in a mempool and lands an hour later still passes its floor, at a price nobody would choose now. Honoured on every arm. SwapRouter02 dropped the field from `exactInputSingle` and takes it through `multicall(uint256,bytes[])` instead, which is what a V3 route is wrapped in; the V2 router takes it directly. UnsignedTx: type: object description: Calldata for a wallet to sign. Nothing here holds a key. properties: to: { type: string } from: { type: string } data: { type: string } value: { type: string } chainId: { type: integer, format: int64 } gasLimit: { type: string } AllowanceRequest: type: object required: [tokenAddress, owner, spender, amount, chainId] properties: tokenAddress: { type: string } owner: { type: string } spender: { type: string } amount: type: string description: The amount the spender needs to move, in the token's smallest unit. chainId: { type: integer, format: int64 } Approval: type: object required: [approved, allowance, needsApproval] properties: approved: { type: boolean } allowance: type: string description: What the chain reports the spender may move today. needsApproval: { type: boolean } approveTx: allOf: [{ $ref: '#/components/schemas/UnsignedTx' }] description: Present only when needsApproval is true. Permit2Request: type: object required: [tokenAddress, owner, spender, amount, deadline, chainId] properties: tokenAddress: { type: string } owner: { type: string } spender: { type: string } amount: { type: string } deadline: { type: integer, format: int64 } chainId: { type: integer, format: int64 } Permit2: type: object required: [permit2Approved, permit2Allowance] properties: permit2Approved: type: boolean description: True only when the grant is both large enough and unexpired. permit2Allowance: { type: string } signatureRequest: type: object description: EIP-712 typed data to sign. Present only when a grant is needed.