Live memecoin data, one token at a time or as a stream
Trenchtick gives you price, market cap, ATH, volume, holder distribution, security and launchpad status for any token on Solana, BSC, Base, Ethereum and the newer EVM chains, over REST or a WebSocket that pushes only what changed.
| Surface | Base URL | Use it for |
|---|---|---|
| REST | https://api.trenchtick.com | Snapshots: look up a token, refresh a list |
| Stream | wss://stream.trenchtick.com/v1/ws | Live updates for the tokens you watch |
Every response is JSON. Prices and volumes are in USD, every *_pct field is a percentage from 0 to 100, supplies and balances are whole tokens, and timestamps are unix seconds unless a field name says _ms.
Quickstart#
Fetch a token with your key. You don't need to know its chain: send the address and we find it.
Then open the stream and subscribe to the same token. You get its full snapshot first, then updates each time something moves. One connection carries your whole watchlist.
Keep your key on your server. Anyone holding it can spend your limits.
curl https://api.trenchtick.com/v1/tokens/GTBxUiw6wJdmmkCGZgRHLyYxqu1vG4KtRpeox6yDpump \
-H "X-API-Key: $TRENCHTICK_KEY"
const res = await fetch( "https://api.trenchtick.com/v1/tokens/GTBxUiw6wJdmmkCGZgRHLyYxqu1vG4KtRpeox6yDpump", { headers: { "X-API-Key": process.env.TRENCHTICK_KEY } } ); const token = await res.json(); console.log(token.symbol, token.market_cap_usd);
import os, requests r = requests.get( "https://api.trenchtick.com/v1/tokens/GTBxUiw6wJdmmkCGZgRHLyYxqu1vG4KtRpeox6yDpump", headers={"X-API-Key": os.environ["TRENCHTICK_KEY"]}, ) token = r.json() print(token["symbol"], token["market_cap_usd"])
const ws = new WebSocket("wss://stream.trenchtick.com/v1/ws?api_key=" + KEY); ws.onopen = () => ws.send(JSON.stringify({ op: "subscribe", tokens: [{ chain: "sol", address: "GTBxUiw6wJdmmkCGZgRHLyYxqu1vG4KtRpeox6yDpump" }], })); ws.onmessage = (e) => { const { type, data } = JSON.parse(e.data); if (type === "snapshots" || type === "updates") console.log(type, data.length); };
Authentication#
Send your key in the X-API-Key header on every REST request. Authorization: Bearer tt_… works too.
The stream takes the same header on the upgrade request. Clients that can't set headers on a WebSocket, such as browsers, can pass ?api_key=tt_… instead.
Keys start with tt_. A missing, unknown, disabled or expired key gets 401.
X-API-Key: tt_3f9a…
# or
Authorization: Bearer tt_3f9a…Rate limits#
Each key has a sustained request rate plus a burst on top. Every REST response tells you what's left. When you run out you get 429 with a Retry-After header in seconds.
| Request | Cost |
|---|---|
GET /v1/tokens/… (either form) | 1 |
POST /v1/tokens/batch (max 10) | 1 per token |
GET /v1/me, GET /v1/chains | free |
| Plan | Sustained | Burst | Stream connections | Stream subscriptions |
|---|---|---|---|---|
| Trial | 3 req/s | +3 | 2 | Unlimited |
| Pro | 5 req/s | +5 | 2 | Unlimited |
Watch as many tokens as you need. One connection holds your whole watchlist; the second is there so you can open a new socket before closing the old one.
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 27
# when limited
HTTP/1.1 429 Too Many Requests
Retry-After: 1Errors#
Every error has the same shape, with a stable code you can branch on and a message for humans.
| Status | Code | Meaning |
|---|---|---|
| 400 | bad_token | Unsupported chain or malformed address |
| 400 | bad_body | Request body isn't the expected JSON |
| 401 | missing_key invalid_key expired_key | Send a valid key |
| 404 | unknown_token | We have no data for this token |
| 429 | rate_limited | Wait Retry-After seconds |
| 502 | upstream | Data is briefly unavailable. Retry. |
| 503 | busy | We're at capacity. Retry shortly. |
{
"error": {
"code": "rate_limited",
"message": "slow down"
}
}Chains#
Use these ids wherever a route or message takes a chain. EVM addresses are case-insensitive and always come back lowercase.
| id | Chain | Address format |
|---|
Token by address#
Returns everything we know about a token when you only have its address. We work out the chain. If the same address exists on more than one chain, which can happen on EVM, you get the one with the deepest liquidity, and that pool is what pool_address, dex and liquidity_usd describe.
Path parameters
| Name | Type | Description |
|---|---|---|
| addressrequired | string | A Solana, Tron or EVM token address |
Response
200 with a Token object. 400 bad_token if the string isn't an address on any supported chain, 404 unknown_token if we have no data.
curl https://api.trenchtick.com/v1/tokens/0xc53d0011343b467b36c8fa25df0ae93062e17777 \
-H "X-API-Key: $TRENCHTICK_KEY"
const token = await fetch( `https://api.trenchtick.com/v1/tokens/${address}`, { headers: { "X-API-Key": KEY } } ).then((r) => r.json());
token = requests.get(
f"https://api.trenchtick.com/v1/tokens/{address}",
headers={"X-API-Key": KEY},
).json()
{
"chain": "bsc",
"address": "0xc53d0011343b467b36c8fa25df0ae93062e17777",
"symbol": "玄学蛙",
"launchpad": "flap",
"pool_address": "0x02c85aa9f03b22c7f5c8d7815a1b2cd4160256c2",
"price_usd": 0.0000027949963,
"liquidity_usd": 5086.92,
"market_cap_usd": 2794.99,
"holders": 93,
"top10_pct": 7.93,
"dev_status": "sold_all",
"sell_tax_pct": 1.0,
"open_source": true,
"honeypot": false
// … every Token field we have
}Token on a chain#
The same full token when you already know the chain. It skips the chain lookup, so it's a little faster. For several tokens at once, use Batch tokens.
Path parameters
| Name | Type | Description |
|---|---|---|
| chainrequired | string | A chain id |
| addressrequired | string | Token address on that chain |
Response
200 with a Token object. 400 bad_token, 404 unknown_token.
curl https://api.trenchtick.com/v1/tokens/sol/GTBxUiw6wJdmmkCGZgRHLyYxqu1vG4KtRpeox6yDpump \
-H "X-API-Key: $TRENCHTICK_KEY"
const token = await fetch( `https://api.trenchtick.com/v1/tokens/${chain}/${address}`, { headers: { "X-API-Key": KEY } } ).then((r) => r.json());
token = requests.get(
f"https://api.trenchtick.com/v1/tokens/{chain}/{address}",
headers={"X-API-Key": KEY},
).json()
{
"chain": "sol",
"address": "GTBxUiw6wJdmmkCGZgRHLyYxqu1vG4KtRpeox6yDpump",
"name": "Jean Phil", "symbol": "JEANPHIL", "decimals": 6,
"socials": { "twitter": "https://x.com/JeanPhilMadame" },
"created_at": 1789841756,
"launchpad": "pump", "launchpad_name": "Pump.fun",
"bonding_curve_pct": 100, "graduated": true,
"dex": "meteora_dlmm", "quote_symbol": "SOL",
"price_usd": 0.0018272455,
"liquidity_usd": 258966.14,
"market_cap_usd": 1771476.17,
"fdv_usd": 1771476.17,
"ath_market_cap_usd": 11589556,
"price_change_1h_pct": -1.09,
"volume_1h_usd": 14767.49, "buys_1h": 75, "sells_1h": 52,
"holders": 16488,
"top10_pct": 12.21, "dev_pct": 1.4063,
"snipers_pct": 1.4063, "bundlers_pct": 19.16,
"fresh_wallets_pct": 5, "bots_pct": 29.24,
"dev_status": "holding",
"mint_renounced": true, "freeze_renounced": true,
"lp_burned_pct": 100, "honeypot": false,
"buy_tax_pct": 0, "sell_tax_pct": 0,
"dex_paid": true, "cto": false
}Batch tokens#
Full tokens for up to 10 addresses in one call. They're looked up in parallel, so a batch answers in about the time of a single lookup. Each entry takes a chain, or leaves it out and we find it, the same way as Token by address.
Body
| Name | Type | Description |
|---|---|---|
| tokensrequired | {address, chain?}[] | 1 to 10 entries. Duplicates count once. |
Response
| Field | Type | Description |
|---|---|---|
| tokens | Token[] | Full Token objects, in request order |
| missing | {chain, address}[] | We have no data for these |
| failed | {chain, address, code}[] | Briefly unavailable (upstream or busy). Retry these. |
One malformed entry rejects the whole request with 400 bad_token before anything is fetched. If every entry fails you get that error (502 / 503) instead of a 200.
curl https://api.trenchtick.com/v1/tokens/batch \ -H "X-API-Key: $TRENCHTICK_KEY" \ -H "Content-Type: application/json" \ -d '{"tokens":[ {"chain":"sol","address":"GTBxUiw6wJdmmkCGZgRHLyYxqu1vG4KtRpeox6yDpump"}, {"address":"0xc53d0011343b467b36c8fa25df0ae93062e17777"} ]}'
const { tokens, missing, failed } = await fetch("https://api.trenchtick.com/v1/tokens/batch", { method: "POST", headers: { "X-API-Key": KEY, "Content-Type": "application/json" }, body: JSON.stringify({ tokens: watchlist.slice(0, 10) }), }).then((r) => r.json());
data = requests.post(
"https://api.trenchtick.com/v1/tokens/batch",
headers={"X-API-Key": KEY},
json={"tokens": watchlist[:10]},
).json()
{
"tokens": [
{ "chain": "sol", "address": "GTBxUiw6wJdmmkCGZgRHLyYxqu1vG4KtRpeox6yDpump",
"symbol": "JEANPHIL", "market_cap_usd": 1771476.17, … full Token … },
{ "chain": "bsc", "address": "0xc53d0011343b467b36c8fa25df0ae93062e17777",
"symbol": "玄学蛙", "market_cap_usd": 2794.99, … full Token … }
],
"missing": [],
"failed": []
}Your key#
Your key's plan, limits, expiry and usage since our last restart. Useful for showing remaining quota in your own tooling.
Response
| Field | Type | Description |
|---|---|---|
| name | string | Your key's name |
| plan | string | trial, pro, … |
| expires_at | number | null | Unix seconds, or null if it doesn't expire |
| limits | object | rps, burst, ws_connections, ws_subscriptions |
| usage | object | requests served, limited refusals |
{
"name": "acme",
"plan": "trial",
"expires_at": 1792200000,
"limits": { "rps": 3, "burst": 3, "ws_connections": 2, "ws_subscriptions": null },
"usage": { "requests": 1240, "limited": 3 }
}Connect#
Authenticate on the upgrade with X-API-Key, or ?api_key= when your client can't set headers. Both checks happen before the socket opens:
- a bad key fails the upgrade with
401; - a key already at its connection limit fails it with
429 connection_limit.
Both use the same JSON error body as REST.
import WebSocket from "ws"; const ws = new WebSocket("wss://stream.trenchtick.com/v1/ws", { headers: { "X-API-Key": process.env.TRENCHTICK_KEY }, });
import websockets ws = await websockets.connect( "wss://stream.trenchtick.com/v1/ws", additional_headers={"X-API-Key": KEY}, )
Subscribe#
One connection carries your whole watchlist, however large. Send JSON text frames. Each carries an op, and optionally an id that we echo back on the reply so you can match them.
| Op | Fields | Reply |
|---|---|---|
set | tokens: up to 20,000 {chain, address} | set with added, removed, tokens |
subscribe | tokens: up to 20,000 | subscribed |
unsubscribe | tokens | unsubscribed |
list | subscriptions on this connection | |
ping | pong |
set: replace the watchlist
The easiest way to manage a watchlist: send the full list every time it changes. We subscribe what's new, drop what's gone and leave the rest alone, so tokens you already had get no second snapshot. An empty list clears everything. For a watchlist over 20,000, send set with the first 20,000 and add the rest with subscribe.
subscribe / unsubscribe
Add or remove tokens incrementally instead. Subscribing to a token you already have is free.
If any token in a message is invalid, nothing in that message changes and you get bad_token.
Ops are limited to about 5 per second per connection, with a burst of 20. Put many tokens in one message.
{
"op": "set",
"tokens": [
{ "chain": "sol", "address": "GTBxUiw6wJdmmkCGZgRHLyYxqu1vG4KtRpeox6yDpump" },
{ "chain": "bsc", "address": "0xc53d0011343b467b36c8fa25df0ae93062e17777" }
],
"id": 1
}{ "type": "set",
"data": { "id": 1, "added": 2, "removed": 0, "tokens": 2 },
"ts": 1791012345678 }Messages#
Every server message has the envelope { type, data, ts }, where ts is our server time in milliseconds.
Token data is batched. Every 250 ms each connection gets at most one snapshots frame and one updates frame, each holding every token that changed in that window. A token that ticked several times in the window appears once, with its latest values. However many tokens you watch, that's a few frames a second on one socket.
snapshots: replace
An array of full Token objects: one per new subscription, usually within a second or two. Replace your copy of each token. Large batches are split across frames of up to 200 tokens.
updates: merge
An array of Tokens holding only chain, address and the fields that changed. Merge each into your copy. A token's updates never arrive before its snapshot. Large batches are split across frames of up to 1,000.
Derived fields arrive with the change that moved them, so a new price comes with its new market_cap_usd, fdv_usd and, if it's a new high, ath_market_cap_usd. A token that isn't trading sends nothing. Which fields update live is in the field reference.
Replies
set, subscribed, unsubscribed, subscriptions, pong and error answer your ops right away, outside the batching. An error carries the code, a message and your id.
{
"type": "updates",
"data": [
{ "chain": "sol", "address": "GTBxUiw6wJdmmkCGZgRHLyYxqu1vG4KtRpeox6yDpump",
"price_usd": 0.0018301, "market_cap_usd": 1774254.9, "fdv_usd": 1774254.9 },
{ "chain": "bsc", "address": "0xc53d0011343b467b36c8fa25df0ae93062e17777",
"holders": 94, "top10_pct": 7.81 }
],
"ts": 1791012345678
}type TokenUpdate = Partial<Token> & { chain: string; address: string }; type ServerMessage = | { type: "snapshots"; data: Token[] } // replace each | { type: "updates"; data: TokenUpdate[] } // merge each | { type: "set"; data: { id?: any; added: number; removed: number; tokens: number } } | { type: "subscribed"; data: { id?: any; added: number; tokens: number } } | { type: "unsubscribed"; data: { id?: any; removed: number } } | { type: "subscriptions"; data: { id?: any; subscriptions: { chain: string; address: string }[] } } | { type: "pong"; data: { id?: any } } | { type: "error"; data: { id?: any; code: string; message: string } }; type ClientMessage = | { op: "set" | "subscribe" | "unsubscribe"; tokens: { chain: string; address: string }[]; id?: any } | { op: "list"; id?: any } | { op: "ping"; id?: any };
Connection rules#
| Rule | What happens |
|---|---|
| Keep-alive | We ping every 30 s, and standard clients answer automatically. A connection silent for 90 s is closed. |
| One socket is enough | A single connection carries your whole watchlist, batched. Your plan allows two so a restart can overlap; see Handling the connection. |
| Slow consumers | Read fast. A client more than 256 frames behind (over 30 s at the batching rate) is closed with code 4000 (dropped: slow consumer). |
| Reconnects | Subscriptions don't survive a reconnect. Send your set again and you'll get fresh snapshots. Back off between attempts. |
| Message size | Client messages are capped at 2 MB, which fits a 20,000-token set. |
Handling the connection#
Run one connection in one place
Hold the stream in a single process on your backend and fan the data out to the rest of your system yourself (your own pub/sub, cache, or your users' websockets). Don't connect from browsers or from every service: your key allows two connections, and it shouldn't leave your servers.
The lifecycle
- Open the socket, then send
setwith your full watchlist. - Apply what arrives: each
snapshotsentry replaces your copy of that token, eachupdatesentry merges into it. - Change the watchlist by sending
setagain with the new full list. Kept tokens are untouched; new ones get a snapshot. - On close, reconnect with exponential backoff and jitter, then send the same
set. The fresh snapshots replace everything you had, so whatever you missed while disconnected is covered; there's nothing to reconcile.
Restarting without a gap
For a deploy or a planned rotation, open the new connection first, send set, wait for its set reply and first snapshots, then close the old one. Both sockets can hold the same watchlist at once.
When something goes wrong
| You see | Do this |
|---|---|
Close 4000 slow consumer | You stopped reading for 30 s or more. Read frames in a loop and hand them to a queue; never do slow work in the message handler. Reconnect. |
429 connection_limit on connect | Both slots are taken, usually by a socket you didn't close. Close it, or wait up to 90 s for us to drop it. |
401 on connect | The key is wrong, disabled or expired. Don't retry in a loop. |
| No frames at all | Quiet tokens send nothing. To check the socket itself, send ping and expect pong within a few seconds. |
async function rotate(oldWs) { const ws = open(); // new socket await once(ws, "open"); ws.send(JSON.stringify({ op: "set", tokens: WATCH, id: "rotate" })); await firstFrame(ws, "snapshots"); // new socket is live oldWs.close(1000); // then drop the old one return ws; }
let attempt = 0; function nextDelay() { const base = Math.min(30000, 1000 * 2 ** attempt++); return base / 2 + Math.random() * base / 2; // jitter } // reset attempt = 0 after the first snapshots arrive
Full example#
A Node client that keeps a local copy of each token, applies batches and reconnects with backoff. It sends its whole watchlist with set, so a reconnect is one message.
import WebSocket from "ws"; const WATCH = [{ chain: "sol", address: "GTBxUiw6wJdmmkCGZgRHLyYxqu1vG4KtRpeox6yDpump" }]; const tokens = new Map(); // "chain:address" → Token let backoff = 1000; function connect() { const ws = new WebSocket("wss://stream.trenchtick.com/v1/ws", { headers: { "X-API-Key": process.env.TRENCHTICK_KEY }, }); ws.on("open", () => { backoff = 1000; ws.send(JSON.stringify({ op: "set", tokens: WATCH })); }); ws.on("message", (raw) => { const { type, data } = JSON.parse(raw); const key = (t) => `${t.chain}:${t.address}`; if (type === "snapshots") for (const t of data) tokens.set(key(t), t); else if (type === "updates") for (const t of data) tokens.set(key(t), { ...tokens.get(key(t)), ...t }); else if (type === "error") console.error(data.code, data.message); }); ws.on("close", () => { setTimeout(connect, backoff); backoff = Math.min(backoff * 2, 30000); }); } connect();
Token object#
Both token routes and every entry of a stream snapshots frame use this shape. An entry of an updates frame is the same object with only chain, address and what changed.
Every field except chain and address is optional. A field we don't have for a token is left out rather than set to null, so check for presence, not truthiness. graduated: false and holders: 0 are real values.
The field reference below describes each field and whether it updates live.
Field reference#
LIVE fields also arrive as stream updates. SNAPSHOT fields come in REST responses and the stream snapshot only.
| Field | Type | Description |
|---|