@quanaris/sdk
v0.1.0
Published
Typed TypeScript SDK for the Quanaris Solana market-data API — REST methods generated from the OpenAPI contract plus an auto-reconnecting WebSocket client.
Downloads
27
Maintainers
Readme
@quanaris/sdk
The typed TypeScript SDK for the Quanaris Solana market-data API — reconciled live + historical OHLCV, normalized swaps and real-time metrics for any Solana token, over a clean REST + WebSocket API.
Birdeye-grade, but with a real SDK. Birdeye ships no first-party typed client and churns its API. Quanaris gives you typed REST methods that match the OpenAPI contract shipped inside this package, plus an auto-reconnecting WebSocket client so you never babysit heartbeats.
- ✅ Fully typed REST methods (
token,wallet,pair,pricing) covering the whole read surface — overview, metrics, holders, security, markets, top traders, trade stats, price history, coverage, portfolio, PnL and more - ✅ camelCase response types, identical across REST and WebSocket
- ✅ Automatic retry/backoff on
429/502/503/network errors for idempotent GETs — exponential backoff + jitter, honoursRetry-After(opt-out) - ✅ Cursor-paginated async iterators (
tradesAll/tradesPages) that follownextCursorfor you - ✅ Auto-reconnecting WS client: 30s ping, mandatory resubscribe on reconnect, replay-on-gap hook
- ✅ Zero runtime dependencies — uses the platform
fetchandWebSocket
Install
npm install @quanaris/sdk
# or: pnpm add @quanaris/sdkRequires Node 18+ (fetch) and, for streaming, Node 22+ (global WebSocket)
or a ws instance passed via WebSocketImpl.
REST quickstart (curl-equivalent, in TS)
import { QuanarisClient } from "@quanaris/sdk";
const q = new QuanarisClient({ apiKey: process.env.QUANARIS_API_KEY });
// curl https://www.quanaris.com/v1/token/<mint>/price
const price = await q.token.price("So11111111111111111111111111111111111111112");
console.log(price.priceUsd, price.updatedAt);
// full market snapshot (price, mcap, liquidity, pools, holders, volume)
const overview = await q.token.overview(mint);
// curl ".../v1/token/<mint>/ohlcv?interval=1m&limit=100"
const { data } = await q.token.ohlcv(mint, { interval: "1m", limit: 100 });
console.log(data.at(-1)); // { t, o, h, l, c, v, trades, confirmed }
// curl ".../v1/token/<mint>/trades?limit=50"
const { trades, nextCursor } = await q.token.trades(mint, { limit: 50 });
// the rest of the token surface — all typed:
await q.token.metadata(mint); // identity + price
await q.token.metrics(mint, { window: 24 }); // rolling-window volume/pressure
await q.token.tradeData(mint); // multi-window activity grid
await q.token.holders(mint); // holder count + top accounts
await q.token.security(mint); // on-chain rug-check
await q.token.markets(mint); // every pool for the token
await q.token.topTraders(mint, { sort: "volume" }); // key-gated
await q.token.priceHistory(mint, { interval: "1h" }); // sparkline points
await q.token.priceAt(mint, { time: 1780521900 }); // price at a past moment
await q.token.coverage(mint); // series confidence / contiguity
// pair / pool reads
const pair = await q.pair.overview(poolAddress);
await q.pair.ohlcv(poolAddress, { interval: "1h" });
// wallet reads
await q.wallet.portfolio(wallet); // holdings valued in USD
await q.wallet.token(wallet, mint); // one token's balance + value
const pnl = await q.wallet.pnl(wallet); // gas-aware realized/unrealized PnL
// batch price a whole portfolio in one call
const prices = await q.pricing.multi([mintA, mintB, mintC]);Every read accepts { network: "mainnet" | "devnet" } (default mainnet), and
you can set a client-wide default:
const q = new QuanarisClient({ apiKey, network: "devnet" });Errors are thrown as a typed QuanarisApiError carrying status, code and
the parsed body:
import { QuanarisApiError } from "@quanaris/sdk";
try {
await q.token.price(badMint);
} catch (e) {
if (e instanceof QuanarisApiError) console.error(e.status, e.code, e.message);
}Need an endpoint not yet wrapped? Use the typed low-level escape hatch:
const me = await q.request<{ plan: string }>("GET", "/v1/me");Resilience: automatic retry & backoff
Idempotent GETs are retried automatically on 429, 502, 503 and
network errors with exponential backoff + jitter, honouring a Retry-After
header when the server sends one. POST/DELETE are never retried. It's on
by default — tune or disable it on the client:
// tune it (defaults: 3 retries, 250ms base delay):
const q = new QuanarisClient({ apiKey, retry: { maxRetries: 5, baseDelayMs: 500 } });
// or turn it off entirely:
const q = new QuanarisClient({ apiKey, retry: false });When a request finally fails with a rate-limit, the thrown QuanarisApiError
carries the parsed RateLimit-* / Retry-After headers:
try {
await q.token.price(mint);
} catch (e) {
if (e instanceof QuanarisApiError && e.status === 429) {
console.log(e.rateLimit); // { remaining, reset, retryAfter }
}
}Pagination: cursor-following async iterators
The trade tapes are keyset-paginated (nextCursor). Instead of looping by hand,
iterate the whole tape — tradesAll yields one trade at a time, tradesPages
yields each page. Both stop at nextCursor: null or when you cap them with
maxItems / maxPages (default cap: 50 pages):
// every trade, newest first, across as many pages as it takes:
for await (const trade of q.token.tradesAll(mint, { maxItems: 1000 })) {
console.log(trade.signature, trade.priceUsd);
}
// page-by-page (e.g. to write each page somewhere):
for await (const page of q.wallet.tradesPages(wallet, { limit: 200 })) {
save(page.trades);
}The same helpers exist on q.token, q.wallet and q.pair.
Streaming quickstart (auto-reconnect)
One socket streams every mint on your account (the v2 account channel).
The client auto-reconnects, pings every 30s, resubscribes automatically, and
hands you the last-seen timestamps on reconnect so you can backfill the gap.
import { QuanarisStream } from "@quanaris/sdk/ws";
import { QuanarisClient } from "@quanaris/sdk";
const q = new QuanarisClient({ apiKey });
const stream = new QuanarisStream({
apiKey, // required for the account channel
interval: "1m",
onTrade: ({ mint, trade }) => console.log("trade", mint, trade.priceUsd),
onCandle: ({ mint, candle }) => console.log("candle", mint, candle.c),
onStateChange: (s) => console.log("ws", s), // connecting | open | reconnecting | closed
onReconnect: async ({ lastSeen }) => {
// replay-on-gap: backfill anything missed while disconnected
for (const [mint, t] of Object.entries(lastSeen)) {
const { data } = await q.token.ohlcv(mint, { interval: "1m", from: t });
// …merge the missed candles…
}
},
});
stream.connect();
stream.account(); // stream ALL your mints over one socketOr subscribe to specific per-mint channels — these are replayed on reconnect too:
stream.connect();
stream.subscribe("ohlcv", mint, "1m");
stream.subscribe("trades", mint);
// later
stream.unsubscribe("ohlcv", mint, "1m");
stream.close(); // stops reconnecting, clears timersOlder Node / custom runtimes
import WebSocket from "ws";
const stream = new QuanarisStream({ apiKey, WebSocketImpl: WebSocket as any });The OpenAPI contract
The machine-readable spec the REST methods are pinned to is committed at
src/openapi.json (also exported as
@quanaris/sdk/openapi.json) and served live at
GET https://www.quanaris.com/openapi.json. The response types in this package
mirror the same canonical camelCase serializers the API emits, so REST and
WebSocket payloads share one shape.
Full reference docs: https://www.quanaris.com/docs.
License
MIT
