> For the complete documentation index, see [llms.txt](https://offchart.gitbook.io/offchart/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://offchart.gitbook.io/offchart/api.md).

# API

The offchart Public API v1 lets bots, trading terminals and Telegram bots work with offchart directly: read the order book and the rounds, follow every swap live, and place orders that **the user's own wallet signs**.

Base URL: `https://useoffchart.xyz/api/v1`. All responses are JSON with `ok: true` on success, or `ok: false` and an `error` code.

{% hint style="info" %}
**Your keys stay yours.** offchart never holds or asks for a wallet's private key. The API builds an unsigned transaction, the wallet signs it, and the API relays it after the same checks the website runs. An API key only raises rate limits. It gives no access to anyone's funds.
{% endhint %}

## Limits and API keys

|               | Limit                                                       |
| ------------- | ----------------------------------------------------------- |
| Without a key | 30 requests per minute per IP address                       |
| With a key    | 300 requests per minute per key                             |
| Live stream   | 1 open connection per key (or per IP address without a key) |

Create a free key in the app: **Wallet menu → API** (or **Profile → API**). Name it, press **Create key** and copy it right away: the key is shown once. offchart stores only its fingerprint. Revoke a key on the same page. A revoked key gets `401 key_revoked` at once.

Send the key in a header:

```
x-api-key: ocpk_…
```

`Authorization: Bearer ocpk_…` works too. Going over the limit returns `429 rate_limited` with a `retry-after` header in seconds. An unknown key returns `401 invalid_key`.

**CORS.** `GET` requests and the stream can be called from any website (`Access-Control-Allow-Origin: *`, no cookies). `POST /orders/*` is not open to browsers on other sites. Call it from your server or bot.

## Order book depth

`GET /tokens/:mint/depth`

Open orders for a token, added up by how far their limit is under the market price: `0-1%`, `1-3%`, `3-5%`, `5-10%`, `10-20%`, `20%+`, and `premium` (at or above the market). The book is split into:

* `sell`: orders selling the token for SOL. `amount` is the tokens left to sell, `valueSol` their value at the market price.
* `buy`: orders buying the token with SOL. `amount` is the SOL left to spend.
* `swap`: token-for-token orders giving this token.

Each bucket has `orders`, `amount`, `amountRaw` (base units), `valueSol` and `cumulativeValueSol`. Hidden orders are not included, and no wallet address is returned.

```bash
curl -s "https://useoffchart.xyz/api/v1/tokens/<MINT>/depth" -H "x-api-key: $OFFCHART_KEY"
```

```js
const r = await fetch(`https://useoffchart.xyz/api/v1/tokens/${mint}/depth`, { headers: { "x-api-key": process.env.OFFCHART_KEY } })
const { depth } = await r.json()
for (const b of depth.sell.buckets) console.log(b.bucket, b.orders, b.amount, b.valueSol)
```

## Rounds

The engine matches orders once a minute. Each round is a list of the swaps it settled: pairs, token-for-token swaps and rings of three, with volume, prices and transaction signatures.

`GET /rounds?limit=20&before=<round id>&withSwaps=1`

* `limit`: 1 to 100 (default 20). Newest first.
* `before`: a round id, for the next page (the response has `next`).
* `withSwaps=1`: only rounds that settled at least one swap.

`GET /rounds/:id` returns one round.

A round has `id`, `ts`, `ordersConsidered`, `pairs`, `rings`, `swaps`, `volumeSol` and `settled`: every swap with `signature`, `solscan`, `kind`, `volumeSol`, `feeSol` and `legs`. A leg is what one order gave and got (`give`, `get`, both with `mint` and `amount`), `priceSol` (SOL per whole token when one side is SOL) and `vsMarketBps` (the execution price against the market at settlement; positive means the token changed hands under the market).

```bash
curl -s "https://useoffchart.xyz/api/v1/rounds?limit=5&withSwaps=1" -H "x-api-key: $OFFCHART_KEY"
curl -s "https://useoffchart.xyz/api/v1/rounds/29904245" -H "x-api-key: $OFFCHART_KEY"
```

```js
const { rounds, next } = await (await fetch("https://useoffchart.xyz/api/v1/rounds?limit=5&withSwaps=1", { headers: { "x-api-key": key } })).json()
const { round } = await (await fetch(`https://useoffchart.xyz/api/v1/rounds/${rounds[0].id}`, { headers: { "x-api-key": key } })).json()
console.log(round.settled.map((s) => s.solscan))
```

## Token stats

`GET /tokens/:mint/stats`

For the last 24 hours (`h24`) and 7 days (`d7`):

* `volumeSol` (and `volumeUsd`), `volume` in tokens: what changed hands through offchart. Each token is counted once, on the seller's side.
* `executions`: settled swaps that moved this token.
* `avgDiscountBps`: the volume-weighted execution price against the market at settlement. Positive means the token traded under the market.
* `slippageAvoidedSol` (and `slippageAvoidedUsd`, `slippageAvoidedBps`): an estimate of what the same trades would have lost in the token's pool.

**How the avoided slippage is estimated.** For each fill in the window we take the token's pool as it is now, as a constant-product pool (x·y=k) holding half of the measured pool liquidity on each side, with a 0.25 % pool fee. For a sale, the estimate is the SOL value at the spot price minus what the pool would pay for the same tokens. For a purchase, it is the tokens the spot price would give minus what the pool would give for the same SOL, valued at the market price. It is an estimate against today's pool, not a replay of the pool at the time of the trade. The same text is in the `method` field of the response.

```bash
curl -s "https://useoffchart.xyz/api/v1/tokens/<MINT>/stats" -H "x-api-key: $OFFCHART_KEY"
```

```js
const { stats } = await (await fetch(`https://useoffchart.xyz/api/v1/tokens/${mint}/stats`, { headers: { "x-api-key": key } })).json()
console.log(stats.h24.volumeSol, stats.h24.slippageAvoidedSol)
```

## Live stream

`GET /stream` is a Server-Sent Events stream with two events:

* `round.settled`: every finished round, about once a minute, even when it settled nothing (`swaps: 0`). It arrives once the round's swaps are final, with the same body as `GET /rounds/:id`.
* `fill`: every settled swap, as soon as it is in the books (same body as an item of `settled`).

The stream opens with a `hello` event and sends a comment line every 15 seconds to stay open. Reconnect on drop (EventSource does it by itself).

```bash
curl -N "https://useoffchart.xyz/api/v1/stream" -H "x-api-key: $OFFCHART_KEY"
```

```js
// Node 18+: read the stream with fetch (EventSource in the browser cannot send a key header; without one the keyless limit applies)
const res = await fetch("https://useoffchart.xyz/api/v1/stream", { headers: { "x-api-key": key } })
const dec = new TextDecoder()
let buf = ""
for await (const chunk of res.body) {
  buf += dec.decode(chunk, { stream: true })
  let i
  while ((i = buf.indexOf("\n\n")) >= 0) {
    const block = buf.slice(0, i); buf = buf.slice(i + 2)
    const ev = /^event: (.+)$/m.exec(block)?.[1]
    const data = /^data: (.+)$/m.exec(block)?.[1]
    if (ev && data) console.log(ev, JSON.parse(data))
  }
}
```

Example event (production, 10 Nov 2026):

```
event: round.settled
id: 1794343024088-1
data: {"id":29905717,"ts":"2026-11-10T20:37:02.944Z","state":"ok","ordersConsidered":0,"plans":0,"pairs":0,"rings":0,"swaps":0,"volumeSol":0,"settled":[]}
```

## Place an order

Two calls: **build**, then **submit** what the wallet signed. The transaction is the same one the website builds: an approval for the exact amount plus the order. Every check of the website applies: balance, the per-order limit, token checks, the risk acknowledgement, blocked wallets.

`POST /orders/build`

| Field      |                                                                                                                                                                    |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `wallet`   | the user's wallet address (it pays the network fee and signs)                                                                                                      |
| `kind`     | `sell` (token → SOL), `buy` (SOL → token) or `swap` (token → other tokens)                                                                                         |
| `giveMint` | what the order gives (`SOL` for a buy)                                                                                                                             |
| `want`     | what it wants: `["SOL"]` for a sell, `[mint]` for a buy, up to 5 mints for a swap                                                                                  |
| `amount`   | amount to give, in base units (lamports for a buy)                                                                                                                 |
| `limitBps` | how far under the market you accept, in bps (300 = 3 %)                                                                                                            |
| optional   | `minOuts` (your own price: base units per wanted mint for the whole amount), `absLimitPrice`, `expiresInDays` (1–90), `partial`, `minFillPct`, `hidden`, `ackRisk` |

The response has `draftId`, `tx` (base64, unsigned), `expiresAt` (5 minutes), `order` and `summary`.

`POST /orders/submit` with `{ "draftId": "…", "signedTx": "<base64>" }`

The signed bytes must be exactly the transaction that was built, signed by the order's wallet and nobody else. A changed amount returns `400 tx_mismatch`, another wallet's signature `400 bad_signature`. The response has `state` (`confirmed`, or `submitted` while the network confirms: send the same call again), `orderId`, `signature` and `solscan`.

```bash
curl -s -X POST "https://useoffchart.xyz/api/v1/orders/build" -H "x-api-key: $OFFCHART_KEY" -H "content-type: application/json" \
  -d '{"wallet":"<WALLET>","kind":"sell","giveMint":"<MINT>","want":["SOL"],"amount":"1000000","limitBps":300}'

curl -s -X POST "https://useoffchart.xyz/api/v1/orders/submit" -H "x-api-key: $OFFCHART_KEY" -H "content-type: application/json" \
  -d '{"draftId":"<DRAFT_ID>","signedTx":"<SIGNED_TX_BASE64>"}'
```

```js
import { Transaction } from "@solana/web3.js"
const post = (path, body) => fetch(`https://useoffchart.xyz/api/v1${path}`, {
  method: "POST", headers: { "x-api-key": key, "content-type": "application/json" }, body: JSON.stringify(body),
}).then((r) => r.json())

const built = await post("/orders/build", { wallet: wallet.publicKey.toBase58(), kind: "sell", giveMint: mint, want: ["SOL"], amount: "1000000", limitBps: 300 })
const tx = Transaction.from(Buffer.from(built.tx, "base64"))
const signed = await wallet.signTransaction(tx) // the user's wallet: Phantom, Solflare, a bot's own keypair…
let res = await post("/orders/submit", { draftId: built.draftId, signedTx: signed.serialize().toString("base64") })
while (res.state === "submitted") res = await post("/orders/submit", { draftId: built.draftId, signedTx: signed.serialize().toString("base64") })
console.log(res.orderId, res.solscan)
```

## Cancel an order

`POST /orders/:id/cancel/build` with `{ "wallet": "…" }` returns an unsigned cancel transaction for the owner's order. Sign it and send it to `POST /orders/submit` like above. Only the order's wallet can build and sign it. An order that is already gone on chain returns `409 not_on_chain` (the books catch up by themselves).

```bash
curl -s -X POST "https://useoffchart.xyz/api/v1/orders/<ORDER_ID>/cancel/build" -H "x-api-key: $OFFCHART_KEY" -H "content-type: application/json" -d '{"wallet":"<WALLET>"}'
```

```js
const c = await post(`/orders/${orderId}/cancel/build`, { wallet: wallet.publicKey.toBase58() })
const signedCancel = await wallet.signTransaction(Transaction.from(Buffer.from(c.tx, "base64")))
console.log(await post("/orders/submit", { draftId: c.draftId, signedTx: signedCancel.serialize().toString("base64") }))
```

## Errors

| Status | `error`                                                     | Meaning                                                                              |
| ------ | ----------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| 400    | `bad_request`, `bad_mint`, `tx_mismatch`, `bad_signature`   | invalid input, or a transaction that is not the one built / not signed by its wallet |
| 401    | `invalid_key`, `key_revoked`                                | the key is unknown or revoked                                                        |
| 403    | `bad_origin`, `wallet_blocked`                              | a browser page of another site, or a blocked wallet                                  |
| 404    | `not_found`                                                 | no such token, round, order or draft                                                 |
| 409    | `not_on_chain`, `risk_ack_required`                         | nothing to cancel on chain; a red-risk token needs `ackRisk: true`                   |
| 410    | `draft_expired`                                             | build again (a draft lives 5 minutes)                                                |
| 422    | `insufficient_balance`, `order_too_large`, `price_required` | the same refusals as on the website                                                  |
| 429    | `rate_limited`, `stream_limit`                              | slow down (see `retry-after`)                                                        |
| 503    | `orders_unavailable`, `intake_off`, `data_unavailable`      | try again later                                                                      |
