Trenchtick v1
Documentation

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.

SurfaceBase URLUse it for
RESThttps://api.trenchtick.comSnapshots: look up a token, refresh a list
Streamwss://stream.trenchtick.com/v1/wsLive 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.

Fetch a token
curl https://api.trenchtick.com/v1/tokens/GTBxUiw6wJdmmkCGZgRHLyYxqu1vG4KtRpeox6yDpump \
  -H "X-API-Key: $TRENCHTICK_KEY"
Stream it
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.

Headers
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.

RequestCost
GET /v1/tokens/… (either form)1
POST /v1/tokens/batch (max 10)1 per token
GET /v1/me, GET /v1/chainsfree
PlanSustainedBurstStream connectionsStream subscriptions
Trial3 req/s+32Unlimited
Pro5 req/s+52Unlimited

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.

Response headers
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 27

# when limited
HTTP/1.1 429 Too Many Requests
Retry-After: 1

Errors#

Every error has the same shape, with a stable code you can branch on and a message for humans.

StatusCodeMeaning
400bad_tokenUnsupported chain or malformed address
400bad_bodyRequest body isn't the expected JSON
401missing_key invalid_key expired_keySend a valid key
404unknown_tokenWe have no data for this token
429rate_limitedWait Retry-After seconds
502upstreamData is briefly unavailable. Retry.
503busyWe're at capacity. Retry shortly.
Error body
{
  "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.

idChainAddress format
REST

Token by address#

GET/v1/tokens/{address}cost 1

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

NameTypeDescription
addressrequiredstringA 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.

Request
curl https://api.trenchtick.com/v1/tokens/0xc53d0011343b467b36c8fa25df0ae93062e17777 \
  -H "X-API-Key: $TRENCHTICK_KEY"
200 · Token
{
  "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
}
REST

Token on a chain#

GET/v1/tokens/{chain}/{address}cost 1

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

NameTypeDescription
chainrequiredstringA chain id
addressrequiredstringToken address on that chain

Response

200 with a Token object. 400 bad_token, 404 unknown_token.

Request
curl https://api.trenchtick.com/v1/tokens/sol/GTBxUiw6wJdmmkCGZgRHLyYxqu1vG4KtRpeox6yDpump \
  -H "X-API-Key: $TRENCHTICK_KEY"
200 · Token
{
  "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
}
REST

Batch tokens#

POST/v1/tokens/batchcost 1 per token

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

NameTypeDescription
tokensrequired{address, chain?}[]1 to 10 entries. Duplicates count once.

Response

FieldTypeDescription
tokensToken[]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.

Request
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"}
      ]}'
200
{
  "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": []
}
REST

Your key#

GET/v1/mefree

Your key's plan, limits, expiry and usage since our last restart. Useful for showing remaining quota in your own tooling.

Response

FieldTypeDescription
namestringYour key's name
planstringtrial, pro, …
expires_atnumber | nullUnix seconds, or null if it doesn't expire
limitsobjectrps, burst, ws_connections, ws_subscriptions
usageobjectrequests served, limited refusals
200
{
  "name": "acme",
  "plan": "trial",
  "expires_at": 1792200000,
  "limits": { "rps": 3, "burst": 3, "ws_connections": 2, "ws_subscriptions": null },
  "usage": { "requests": 1240, "limited": 3 }
}
REST

List chains#

GET/v1/chainsfree

The chains we currently support, with display names. The same list as Chains, kept current.

Response

FieldTypeDescription
chains{id, name}[]Chain id and display name
200
{
  "chains": [
    { "id": "sol", "name": "Solana" },
    { "id": "bsc", "name": "BNB Chain" },
    …
  ]
}
Stream

Connect#

WSwss://stream.trenchtick.com/v1/ws

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.

Connect
import WebSocket from "ws";

const ws = new WebSocket("wss://stream.trenchtick.com/v1/ws", {
  headers: { "X-API-Key": process.env.TRENCHTICK_KEY },
});
Stream

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.

OpFieldsReply
settokens: up to 20,000 {chain, address}set with added, removed, tokens
subscribetokens: up to 20,000subscribed
unsubscribetokensunsubscribed
listsubscriptions on this connection
pingpong

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.

Client → server
{
  "op": "set",
  "tokens": [
    { "chain": "sol", "address": "GTBxUiw6wJdmmkCGZgRHLyYxqu1vG4KtRpeox6yDpump" },
    { "chain": "bsc", "address": "0xc53d0011343b467b36c8fa25df0ae93062e17777" }
  ],
  "id": 1
}
Server → client
{ "type": "set",
  "data": { "id": 1, "added": 2, "removed": 0, "tokens": 2 },
  "ts": 1791012345678 }
Stream

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.

updates
{
  "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
}
Types
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 };
Stream

Connection rules#

RuleWhat happens
Keep-aliveWe ping every 30 s, and standard clients answer automatically. A connection silent for 90 s is closed.
One socket is enoughA single connection carries your whole watchlist, batched. Your plan allows two so a restart can overlap; see Handling the connection.
Slow consumersRead fast. A client more than 256 frames behind (over 30 s at the batching rate) is closed with code 4000 (dropped: slow consumer).
ReconnectsSubscriptions don't survive a reconnect. Send your set again and you'll get fresh snapshots. Back off between attempts.
Message sizeClient messages are capped at 2 MB, which fits a 20,000-token set.
Stream

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

  1. Open the socket, then send set with your full watchlist.
  2. Apply what arrives: each snapshots entry replaces your copy of that token, each updates entry merges into it.
  3. Change the watchlist by sending set again with the new full list. Kept tokens are untouched; new ones get a snapshot.
  4. 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 seeDo this
Close 4000 slow consumerYou 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 connectBoth 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 connectThe key is wrong, disabled or expired. Don't retry in a loop.
No frames at allQuiet tokens send nothing. To check the socket itself, send ping and expect pong within a few seconds.
Zero-gap rotation
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;
}
Reconnect backoff
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
Stream

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.

watch.mjs
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();
Reference

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.

TypeScript
Reference

Field reference#

LIVE fields also arrive as stream updates. SNAPSHOT fields come in REST responses and the stream snapshot only.

FieldTypeDescription