TheDesperateTrader Partner API, v1
Machine readable access to our scanner's board intelligence: which symbols we surfaced, which boards each one landed on, how many boards it appears on (confluence), and our ranking.
You bring your own market data. We give you the list.
Base URL: https://thedesperatetrader.com
1. Quickstart
Replace YOUR_KEY with the key you were sent, paste into a terminal, done:
curl -H "X-API-Key: YOUR_KEY" \
"https://thedesperatetrader.com/api/v1/boards?timeframe=day&limit=10"That is the whole API surface. One endpoint, one header, JSON out.
2. Authentication
Your API key is the only credential you need. There is no secret, no signature, no account ID, no OAuth step, no token exchange, and no expiry. Send the key on every request.
Two accepted ways to send it. They behave identically, pick one:
| Method | Header |
|---|---|
| Preferred | X-API-Key: YOUR_KEY |
| Also accepted | Authorization: Bearer YOUR_KEY |
Keys look like dtk_live_ followed by roughly 40 more characters. Treat it like a password.
The same request in every language
curl
curl -H "X-API-Key: YOUR_KEY" \
"https://thedesperatetrader.com/api/v1/boards?timeframe=day&limit=10"Python (requests)
import requests
API_KEY = "YOUR_KEY"
BASE = "https://thedesperatetrader.com"
HEADERS = {"X-API-Key": API_KEY} # send this on EVERY call
r = requests.get(f"{BASE}/api/v1/boards",
headers=HEADERS,
params={"timeframe": "day", "limit": 10},
timeout=15)
r.raise_for_status()
data = r.json()
print("as of", data["asOf"], "| session:", data["session"])
if data["topPlay"]:
print("top play:", data["topPlay"]["symbol"], data["topPlay"]["reasons"])
for row in data["boards"].get("movers", []):
print(row["rank"], row["symbol"], row["dayPct"], row["metric"])Node (fetch, built in on Node 18+)
const API_KEY = 'YOUR_KEY';
const BASE = 'https://thedesperatetrader.com';
const res = await fetch(`${BASE}/api/v1/boards?timeframe=day&limit=10`, {
headers: { 'X-API-Key': API_KEY }, // send this on EVERY call
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const data = await res.json();
console.log(data.topPlay?.symbol, data.topPlay?.reasons);
for (const row of data.boards.movers ?? []) {
console.log(row.rank, row.symbol, row.dayPct);
}PHP
$ch = curl_init("https://thedesperatetrader.com/api/v1/boards?timeframe=day&limit=10");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-API-Key: YOUR_KEY"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $data["topPlay"]["symbol"];Authentication failures
| Status | Meaning | Fix |
|---|---|---|
401 | No key sent, or not recognised | Check the header spelling and that the key was pasted whole |
403 | Valid key, but revoked | Contact us |
3. The endpoint
GET /api/v1/boards
Current snapshot of every live board for one timeframe, plus the cross board Top Play.
| Parameter | Type | Default | Allowed | Notes |
|---|---|---|---|---|
timeframe | string | day | scalp, day, swing, position, investing | An unrecognised value falls back to day rather than erroring |
limit | integer | 10 | 1 to 10 | Rows per board. Above 10 is clamped, not rejected |
Each timeframe is a separate scan with its own boards, its own scoring lookback and its own topPlay, not the same list re-sorted. scalp scores on the last 1 to 5 minutes, day on the session, swing on roughly 5 to 20 days, position and investing on progressively longer horizons. Poll whichever match your holding period. Polling all five is fine within your rate limit.
4. Exactly what data is sent
Top level
| Field | Type | Description |
|---|---|---|
asOf | string | ISO 8601 UTC timestamp of when this snapshot was built. Use it to tell whether anything actually changed |
timeframe | string | The timeframe actually served, echoed back after validation |
session | string or null | Market session at build time: premarket, open, afterhours, closed |
breadth | object or null | Market breadth at build time |
topPlay | object or null | Highest confluence name across all boards. Null when nothing qualifies |
boards | object | Map of board name to array of rows |
boardNames | string[] | The keys present in boards for this response, so you can iterate without hardcoding |
breadth
| Field | Type | Description |
|---|---|---|
advancers | number or null | Symbols up on the session |
decliners | number or null | Symbols down on the session |
symbols | number or null | Total symbols in the scanned universe |
topPlay
The name appearing on the most boards at once. This is the headline pick.
| Field | Type | Description |
|---|---|---|
symbol | string | Ticker |
name | string or null | Company name |
price | number or null | Last price, USD |
dayPct | number or null | Percent change over the timeframe window. 12.4 means +12.4% |
volume | number or null | Volume in shares |
boards | number | How many boards this symbol appears on. This is the confluence score |
bias | string | bullish, bearish, or neutral |
reasons | string[] | Human readable labels behind the pick, for example ["High RVOL", "Top mover"] |
Board rows
Every array inside boards contains objects of exactly this shape, no more and no less:
| Field | Type | Description |
|---|---|---|
rank | number | Position within this board, starting at 1 |
symbol | string | Ticker |
name | string or null | Company name |
price | number or null | Last price, USD |
dayPct | number or null | Percent change over the timeframe window |
volume | number or null | Volume in shares |
boards | number | How many boards this symbol appears on overall. Use it to compute your own confluence |
bias | string | bullish, bearish, or neutral |
metric | string or null | This board's own headline stat, pre-formatted. See the note below |
tag | string or null | Short qualifier, for example "Low float" |
On `metric`. Each board ranks on its own number, pre-formatted for display ("12.4x RVOL", "$1.2B traded today"). Treat it as a label, not a float. If you need a number, derive it from price, dayPct and volume, which are always real numbers when present.
On nulls. Any field marked "or null" can genuinely be null when the underlying data is missing or stale for that symbol. Do not assume price or dayPct is always populated. Thinly traded, newly listed and halted names are the usual cause.
On array lengths. Read each array's length rather than assuming limit. Any board can come back short when not enough names qualify, and emaTrend, lowFloat and google routinely do.
Example response (trimmed to two boards)
{
"asOf": "2026-07-24T20:15:03.412Z",
"timeframe": "day",
"session": "closed",
"breadth": { "advancers": 3182, "decliners": 4471, "symbols": 8106 },
"topPlay": {
"symbol": "STAK",
"name": "Solidion Technology Inc",
"price": 11.40,
"dayPct": 858.0,
"volume": 412000000,
"boards": 6,
"bias": "bullish",
"reasons": ["Top mover", "High RVOL", "Low float", "Alerted"]
},
"boards": {
"movers": [
{ "rank": 1, "symbol": "STAK", "name": "Solidion Technology Inc",
"price": 11.40, "dayPct": 858.0, "volume": 412000000,
"boards": 6, "bias": "bullish", "metric": "+858.0%", "tag": "Low float" }
],
"rvol": [
{ "rank": 1, "symbol": "OMH", "name": "Ohmyhome Ltd",
"price": 2.19, "dayPct": 500.0, "volume": 88000000,
"boards": 4, "bias": "bullish", "metric": "31.4x RVOL", "tag": null }
]
},
"boardNames": ["movers", "rvol"]
}5. Board catalogue
Always read boardNames rather than hardcoding this table. We add boards over time and they appear automatically.
| Board key | Ranks on | scalp | day | swing | position | investing |
|---|---|---|---|---|---|---|
alerted | alert count (distance from 52 week high on the daily frames) | ● | ● | ● | ● | ● |
movers | move over the timeframe's window | ● | ● | ● | ● | ● |
rvol | relative volume versus average | ● | ● | ● | ● | ● |
momentum | volume acceleration and recent thrust | ● | ● | ● | ● | ● |
emaTrend | holding above the 9, 200 minute and 200 day | ● | ● | ● | ● | ● |
reddit | WSB buzz | ● | ● | ● | ● | ● |
google | search volume | ● | ● | ● | ● | ● |
moneyFlow | dollars traded, 6 per cap tier | ● | ● | ● | ● | |
lowFloat | float size | ● | ● | |||
shorted | short percent of float | ● | ● | |||
smartMoney | 13F funds adding | ● | ● | ● | ||
insiderBuys | Form 4 insider buys | ● | ● | ● | ||
buzz | alert days over 14 days | ● | ● | ● | ||
steady | steady climbers | ● | ● | ● | ||
relStrength | relative strength versus SPY | ● | ● | ● | ||
breakouts | base breakout | ● | ● | ● | ||
pullback | at a rising 50 day | ● | ● | ● | ||
highClub | fresh 52 week high on volume | ● | ● | ● |
A symbol appearing on several boards at once is the signal worth watching. That count is the boards field on every row, and it is what drives the topPlay pick.
Volume you can expect per call at `limit=10` (counts shift with the tape):
| Timeframe | Boards | Rows per call |
|---|---|---|
scalp | 9 | around 79 |
day | 12 | around 122 |
swing | 15 | around 162 |
position | 16 | around 170 |
investing | 15 | around 162 |
Polling all five gives roughly 695 rows across 67 boards.
Two structural exceptions to limit
- `moneyFlow` always returns 24 rows regardless of
limit. It is the top 6 in each of four
market cap tiers (blue, mid, small, penny), so slicing it would drop entire tiers and make the board meaningless.
- `google` often returns fewer than 10. It reads a search trending window sized to the
timeframe (4 hours on scalp, longer on the daily frames), and only so many finance names trend in a short window. That is real scarcity, not a cap.
How confluence is scored
The confluence score is always computed over each board's top 5, whatever limit you request. A name sitting 9th on two boards is not the same signal as one sitting 2nd on two boards. Asking for more rows gives you more candidates without inflating their scores or moving the Top Play.
6. Rate limits and caching
Limit: 120 requests per minute per key, fixed one minute window.
Every response carries your current budget:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118| Header | Meaning |
|---|---|
X-RateLimit-Limit | Your ceiling per minute |
X-RateLimit-Remaining | Requests left in the current window |
Retry-After | Seconds until the window resets. Sent only on a 429 |
X-Cache | HIT if you got a cached copy, MISS if it was freshly built |
Throttled requests are not billable.
Caching: responses are cached server side for about 45 seconds per timeframe plus limit combination, and boards recompute roughly once a minute.
Polling advice: polling faster than once every 30 to 60 seconds returns identical bytes and just burns rate limit. Once a minute per timeframe is the sweet spot. All five timeframes continuously is 5 calls a minute out of your 120. Check asOf to tell whether anything actually changed.
7. Errors
All errors return JSON shaped { "error": "message" }:
{ "error": "Rate limit exceeded (120 requests/minute)." }| Status | Meaning | What to do |
|---|---|---|
401 | Missing or unrecognised key | Check the header name and the key value |
403 | Key recognised but revoked | Contact us |
429 | Rate limit exceeded | Wait Retry-After seconds, then retry |
503 | Boards temporarily unavailable | Retry in about 30 seconds. Transient |
Retry 429 and 503 with backoff. Treat 401 and 403 as permanent until the key is fixed:
import time, requests
API_KEY = "YOUR_KEY"
def get_boards(timeframe="day", limit=10, tries=4):
for attempt in range(tries):
r = requests.get("https://thedesperatetrader.com/api/v1/boards",
headers={"X-API-Key": API_KEY},
params={"timeframe": timeframe, "limit": limit},
timeout=15)
if r.status_code == 200:
return r.json()
if r.status_code in (401, 403):
raise RuntimeError(f"auth problem: {r.status_code} {r.text}")
time.sleep(int(r.headers.get("Retry-After", 2 ** attempt)))
raise RuntimeError("giving up after retries")8. Recipes
The Top Play on every timeframe
for tf in scalp day swing position investing; do
curl -s -H "X-API-Key: YOUR_KEY" \
"https://thedesperatetrader.com/api/v1/boards?timeframe=$tf" \
| python3 -c "import sys,json;d=json.load(sys.stdin);t=d['topPlay'];print(d['timeframe'], t['symbol'] if t else '-', t['reasons'] if t else '')"
doneBuild your own confluence: symbols on 3 or more boards
from collections import defaultdict
data = get_boards("day", limit=10)
hits = defaultdict(list)
for board_name, rows in data["boards"].items():
for row in rows:
hits[row["symbol"]].append(board_name)
for symbol, boards in sorted(hits.items(), key=lambda kv: -len(kv[1])):
if len(boards) >= 3:
print(f"{symbol:6} {len(boards)} boards: {', '.join(boards)}")Iterate every board without hardcoding names
data = get_boards("swing")
for name in data["boardNames"]:
rows = data["boards"][name]
print(f"\n{name} ({len(rows)} rows)")
for r in rows:
print(f" {r['rank']:2}. {r['symbol']:6} {r['dayPct']} {r['metric'] or ''}")Watch one board, staying inside the cache
import time
while True:
for r in get_boards("day", limit=10)["boards"].get("rvol", [])[:5]:
print(f"{r['rank']}. {r['symbol']:6} {r['metric']}")
time.sleep(60) # matches the ~45s server cache9. Scope
What this is not:
- No price history, candles or backfill.
price,dayPctandvolumeare point in time
context for a ranking, not a data feed. Pull OHLCV from your own broker.
- No trade signals or positions. We never expose our own book, pending orders or fills.
- No alert stream. Current state only.
Terms: informational only, not investment advice. No redistribution or resale of the response. No uptime SLA on v1, so build in retries and treat a 5xx as "try again shortly". The key identifies your account, so do not share or publish it. If it leaks, tell us and we will rotate it. You are responsible for your own trading decisions and risk.
10. Support
Questions, a key that stopped working, or a board you would like added, open a ticket here:
[thedesperatetrader.com/support](https://thedesperatetrader.com/support)
That goes straight to us and gives you a thread we can both follow, which is faster than email.
*v1. Additive changes such as new boards or new fields can ship at any time, which is why you should read boardNames and ignore unknown fields rather than pinning to a fixed list.*