Files
dex/api/openapi.yaml
zeekayandHanzo Dev d0d8a84e03 Being busy said the chain was down
Nothing could be read, so it answered that the chain could not be reached. Under
load that is this deployment running out of its own time budget with the chain
answering normally the whole while, and the two send a reader to different
places. A timeout says so and names what to do about it.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-09 12:35:38 -07:00

707 lines
26 KiB
YAML

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.