Read what a coin's holders say, from your own code
The same reads /dd gives you on Discord and Telegram, as JSON. One call per coin returns what it is according to the people holding it, who they say is behind it, why they are in, and what argues against.
Get a key
- Run /api in Discord, or in a private chat with the bot on Telegram.
- Press Create key and give it a name. Copy the key straight away: it is shown once, and only a fingerprint of it is kept.
- Send it with every request, as below.
Keys come with Pro and Max. A key belongs to the account that made it: one made on Discord spends that Discord account's plan, and one made on Telegram spends the Telegram account's. Each account can hold three keys at once. Keys start with srb_; one made before the bot was renamed starts with ddb_ and keeps working.
Authentication
Send the key as a bearer token on every request.
curl https://api.shadysresearchbot.com/v1/usage \
-H "Authorization: Bearer srb_7Hq2kLm9xR4vTn8pWb3cYs6dFg1hJz5aQe0uNo"
Keep keys on a server. Anyone holding one spends your reads, so never put it in a browser app, a public repo or a shared notebook. If one leaks, revoke it from /api and it stops working at once.
Endpoints
Four endpoints, all GET. Only the coin report spends a read.
| Endpoint | Returns | Costs |
|---|---|---|
| /v1/coins/{address} | A coin's report | 1 read |
| /v1/coins/{address}/holders | Top holders who also posted | free |
| /v1/jobs/{job} | A report still being read | free |
| /v1/usage | Your plan and what is left | free |
/v1/coins/{address}
1 readThe full report, the same one the bot draws. A coin somebody has already read comes back at once from the store. One nobody has read yet is read now, which takes a while: the request waits up to 60 seconds and, if the read is still going, answers 202 with a job to poll.
| Parameter | In | What it does |
|---|---|---|
| address | path | A Solana mint, or a 0x address on an EVM chain. |
| chain | query, optional | solana, ethereum, base, bsc, arbitrum, robinhood or arc. Leave it out and the chain is worked out for you: a Solana address by its shape, a 0x address by looking it up. If it cannot be placed you get COIN_NOT_PLACED, never a guess. |
| fresh | query, optional | true skips the stored report and reads the coin again now. Always charged, even on a coin you read today. Once per coin every 5 minutes. |
| wait | query, optional | Seconds to hold the request open for a read in progress, 0 to 60. Default 60. Send 0 to get a job straight away. |
curl https://api.shadysresearchbot.com/v1/coins/4kM2pB8cQyRZ3n5WdT1sHvLx6eJfGa9uN7Vqpump \
-H "Authorization: Bearer $SRB_KEY"
{
"report": {
"address": "4kM2pB8cQyRZ3n5WdT1sHvLx6eJfGa9uN7Vqpump",
"chain": "solana",
"verdict": "clear",
"headline": "A cat coin held for its community, with no product claimed.",
"story": "Launched on pump.fun as a joke about ...",
"utility": "",
"team": "Holders say the dev is anonymous and has not sold.",
"whyBuying": ["the community", "early holders are still in"],
"concerns": ["no product", "one wallet holds a large share"],
"links": [
{ "url": "https://x.com/...", "host": "x.com", "kind": "tweet", "count": 9, "authors": 7 }
],
"callers": [ ... ],
"stats": { "callers": 42, "distinctAuthors": 31, "medianPositionUsd": 380 },
"cached": true,
"generatedAt": "2026-10-08T15:02:11Z"
},
"quota": {
"charged": true,
"freeRepeat": false,
"day": { "used": 7, "limit": 15 },
"month": { "used": 59, "limit": 200 }
}
}
{
"status": "reading",
"job": "job_9fQ2xLw4",
"poll": "/v1/jobs/job_9fQ2xLw4",
"retryAfter": 5
}
/v1/coins/{address}/holders
freeThe holders who also posted, from pump.fun and FOMO, as the bot's Holders button shows them. Each row's rank is its place on that app's own list, not among every wallet on chain, so ranks of 1, 4 and 5 mean the 2nd and 3rd biggest positions never wrote a word. A snapshot stands for five minutes. Takes the same chain parameter.
{
"address": "4kM2pB8cQyRZ3n5WdT1sHvLx6eJfGa9uN7Vqpump",
"chain": "solana",
"fetchedAt": "2026-10-08T15:04:00Z",
"pumpfun": {
"total": 590,
"totalIs": "positions",
"holders": [
{ "rank": 1, "handle": "catdad", "valueUsd": 4210, "thesis": "Here since 20k ..." }
]
},
"fomo": { "total": 212, "totalIs": "holders", "holders": [ ... ] }
}
/v1/jobs/{job}
freePoll a read that was still going when /coins answered 202. While it runs you get 202 again, with retryAfter. When it is done you get exactly what /coins would have returned. The read is charged once, when it finishes, and a read that fails is never charged. Jobs are kept for 15 minutes.
/v1/usage
freeYour plan, what is left, and when it comes back. Call it as often as you like: it only counts toward the speed limit.
{
"account": { "platform": "discord", "plan": "pro", "expiresAt": "2026-11-08T14:02:00Z" },
"day": { "used": 6, "limit": 15, "resetsAt": "2026-10-09T00:00:00Z" },
"month": { "used": 58, "limit": 200, "resetsAt": "2026-10-21T09:14:00Z", "throughApi": 23 },
"perMinute": { "limit": 10, "remaining": 9 },
"keys": 2
}
What counts as a read
- One read per coin, per account, per day. Asking for a coin again the same day is free, wherever you read it first: /dd or the API, on the same account.
- A fresh read is always charged.
fresh=truehere, or Re-read in the bot, pays for a new read even on a coin you read today. - A read that fails is never charged. If a coin cannot be placed or nothing could be read, nothing is taken.
- Holders, jobs and usage never spend a read. They only count toward the speed limit.
- Reads are shared with the bot. Pro's 200 a month is 200 between /dd and the API together.
A day ends at midnight UTC. A month is 30 days, counted from when you first bought your plan, and changing plan never restarts it.
Limits
| Plan | Reads a day | Reads a month | Requests a minute |
|---|---|---|---|
| Pro · $40 a month | 15 | 200 | 10 |
| Max · $99 a month | 50 | 1,000 | 30 |
The speed limit is per account, across all its keys. Every response says what is left, so you never have to guess:
X-Reads-Day-Remaining: 9
X-Reads-Month-Remaining: 142
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 9
Retry-After: 6 (only on a 429)
Errors
Every error is JSON, with a code to switch on and a sentence a person can read.
{
"error": {
"code": "DAILY_LIMIT",
"message": "You have used today's 15 reads. They come back at midnight UTC.",
"resetsAt": "2026-10-09T00:00:00Z"
}
}
| Status | Code | When |
|---|---|---|
| 400 | BAD_ADDRESS | That is not a coin address on any chain this reads. |
| 401 | INVALID_KEY | The key is missing, mistyped or revoked. |
| 403 | PLAN_REQUIRED | The account is on Free, or its plan ran out. Its keys work again once it is renewed. |
| 403 | ACCOUNT_BLOCKED | The account was shut out by the bot's owner. Nothing was charged. |
| 404 | JOB_NOT_FOUND | No such job, or it is older than 15 minutes. |
| 405 | METHOD_NOT_ALLOWED | Anything but GET on a /v1 route, HEAD included. Nothing was read or charged. |
| 422 | COIN_NOT_PLACED | A 0x address no supported chain knows. Pass chain if you know it. |
| 429 | RATE_LIMITED | Over your requests a minute. Wait retryAfter seconds. |
| 429 | DAILY_LIMIT | Today's reads are used. resetsAt says when they come back. |
| 429 | MONTHLY_LIMIT | This month's reads are used. resetsAt says when they come back. |
| 429 | COOLDOWN | fresh=true on a coin read fresh in the last 5 minutes. The answer would be the same anyway. |
| 503 | UNAVAILABLE | The API is down or paused. Nothing was charged. |
| tool result | NOT_READ | coin_links or coin_sources on a coin this account has not read. Call read_coin first. |
| tool result | BAD_QUERY | find_coin with fewer than two characters, or a chain it does not know. |
Use it from an AI agent
The API is also an MCP server, so Claude and other agents can read coins for you with the same key, the same reads and the same limits. Seven tools; only read_coin spends a read, and only tool calls count toward the speed limit. An agent waits out a fresh read itself, so there is no job to poll.
Connect me to Shady's Research Bot, an MCP server that reads what a coin's holders say about it.
Name: shadysresearchbot
Transport: Streamable HTTP
URL: https://api.shadysresearchbot.com/mcp
Header: Authorization: Bearer srb_YOUR_KEY
If you are Claude Code, Cursor, Codex or another client that can edit its own config, add it yourself:
Claude Code: claude mcp add --transport http --scope user shadysresearchbot https://api.shadysresearchbot.com/mcp --header "Authorization: Bearer srb_YOUR_KEY"
mcp.json: {"mcpServers": {"shadysresearchbot": {"type": "http", "url": "https://api.shadysresearchbot.com/mcp", "headers": {"Authorization": "Bearer srb_YOUR_KEY"}}}}
Then call its get_usage tool and tell me which plan I am on and how many reads I have left today.
If you are claude.ai or Claude Desktop, you cannot add connectors yourself, so walk me through it step by step:
1. Open Customize, then Connectors, press + Add, then Add custom connector.
2. Name: Shady's Research Bot
3. URL: https://api.shadysresearchbot.com/mcp/srb_YOUR_KEY
4. Authentication: No sign-in. Leave every OAuth field empty.
5. Add it, start a new chat, and ask me to call get_usage.
That URL carries my key, so treat it like a password.
Do not commit the key or write it into any file tracked by git.
Paste it into Claude Code, Cursor or Codex and the agent adds the server itself, then calls get_usage. Paste it into claude.ai or Claude Desktop and Claude walks you through adding it as a custom connector. Replace ddb_YOUR_KEY with your key: the Key created screen in the bot shows this prompt with your key already in it.
claude.ai and Claude Desktop
- Open Customize, then Connectors, press + Add, then Add custom connector.
- Name: Shady's Research Bot
- URL: your personal connector URL
- Authentication: No sign-in. Leave every OAuth field empty.
- Add it, start a new chat, and ask Claude to call get_usage.
Your personal connector URL is https://api.shadysresearchbot.com/mcp/<your key>. The key rides in the address because those apps cannot send a header without a sign-in flow, so treat the URL like a password. Revoking the key from /api kills it at once, and a bad or revoked key answers 401. GET and DELETE on it answer 405.
By hand
claude mcp add --transport http --scope user shadysresearchbot https://api.shadysresearchbot.com/mcp --header "Authorization: Bearer srb_YOUR_KEY"
{
"mcpServers": {
"shadysresearchbot": {
"type": "http",
"url": "https://api.shadysresearchbot.com/mcp",
"headers": {
"Authorization": "Bearer srb_YOUR_KEY"
}
}
}
}
The tools
| Tool | What it does | Costs |
|---|---|---|
| read_coin | A coin's compact report: headline, story, what it does and who is behind it as holders tell it, why they are in, what argues against, counts, and what its own pages say. | 1 read |
| coin_holders | The holders who also posted, with their rank on each app's own list and their own words. | free |
| coin_links | Every link holders shared, ranked by people before mentions, a page at a time. After read_coin. | free |
| coin_sources | The coin's own site and X account as read for the report, what could not be read and why, and which claims rest on them. After read_coin. | free |
| find_coin | Candidate addresses for a ticker, a name or the start of an address, from the coins the bot already holds posts for. | free |
| recent_reads | The coins this account read lately, and which are free to ask again today. | free |
| get_usage | The plan, the reads left today and this month, and the speed limit. | free |
A refused tool call is a result the agent can read, not a transport error: a JSON body with code and message, the same codes as above, plus NOT_READ for coin_links and coin_sources on a coin this account has not read yet, and BAD_QUERY for a find_coin query it cannot search. A bad key is still 401 before anything runs.