Subscriptions are live: We're on the subscription model now, early-access price $24.99/month, with day, week, and longer plans too. New accounts start with 5 free premium days. Prefer to donate? Donations grant premium time and are logged on your profile page.

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:

bash
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:

MethodHeader
PreferredX-API-Key: YOUR_KEY
Also acceptedAuthorization: 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

bash
curl -H "X-API-Key: YOUR_KEY" \
  "https://thedesperatetrader.com/api/v1/boards?timeframe=day&limit=10"

Python (requests)

python
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+)

js
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

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

StatusMeaningFix
401No key sent, or not recognisedCheck the header spelling and that the key was pasted whole
403Valid key, but revokedContact us

3. The endpoint

GET /api/v1/boards

Current snapshot of every live board for one timeframe, plus the cross board Top Play.

ParameterTypeDefaultAllowedNotes
timeframestringdayscalp, day, swing, position, investingAn unrecognised value falls back to day rather than erroring
limitinteger101 to 10Rows 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

FieldTypeDescription
asOfstringISO 8601 UTC timestamp of when this snapshot was built. Use it to tell whether anything actually changed
timeframestringThe timeframe actually served, echoed back after validation
sessionstring or nullMarket session at build time: premarket, open, afterhours, closed
breadthobject or nullMarket breadth at build time
topPlayobject or nullHighest confluence name across all boards. Null when nothing qualifies
boardsobjectMap of board name to array of rows
boardNamesstring[]The keys present in boards for this response, so you can iterate without hardcoding

breadth

FieldTypeDescription
advancersnumber or nullSymbols up on the session
declinersnumber or nullSymbols down on the session
symbolsnumber or nullTotal symbols in the scanned universe

topPlay

The name appearing on the most boards at once. This is the headline pick.

FieldTypeDescription
symbolstringTicker
namestring or nullCompany name
pricenumber or nullLast price, USD
dayPctnumber or nullPercent change over the timeframe window. 12.4 means +12.4%
volumenumber or nullVolume in shares
boardsnumberHow many boards this symbol appears on. This is the confluence score
biasstringbullish, bearish, or neutral
reasonsstring[]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:

FieldTypeDescription
ranknumberPosition within this board, starting at 1
symbolstringTicker
namestring or nullCompany name
pricenumber or nullLast price, USD
dayPctnumber or nullPercent change over the timeframe window
volumenumber or nullVolume in shares
boardsnumberHow many boards this symbol appears on overall. Use it to compute your own confluence
biasstringbullish, bearish, or neutral
metricstring or nullThis board's own headline stat, pre-formatted. See the note below
tagstring or nullShort 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)

json
{
  "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 keyRanks onscalpdayswingpositioninvesting
alertedalert count (distance from 52 week high on the daily frames)
moversmove over the timeframe's window
rvolrelative volume versus average
momentumvolume acceleration and recent thrust
emaTrendholding above the 9, 200 minute and 200 day
redditWSB buzz
googlesearch volume
moneyFlowdollars traded, 6 per cap tier
lowFloatfloat size
shortedshort percent of float
smartMoney13F funds adding
insiderBuysForm 4 insider buys
buzzalert days over 14 days
steadysteady climbers
relStrengthrelative strength versus SPY
breakoutsbase breakout
pullbackat a rising 50 day
highClubfresh 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):

TimeframeBoardsRows per call
scalp9around 79
day12around 122
swing15around 162
position16around 170
investing15around 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
HeaderMeaning
X-RateLimit-LimitYour ceiling per minute
X-RateLimit-RemainingRequests left in the current window
Retry-AfterSeconds until the window resets. Sent only on a 429
X-CacheHIT 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" }:

json
{ "error": "Rate limit exceeded (120 requests/minute)." }
StatusMeaningWhat to do
401Missing or unrecognised keyCheck the header name and the key value
403Key recognised but revokedContact us
429Rate limit exceededWait Retry-After seconds, then retry
503Boards temporarily unavailableRetry in about 30 seconds. Transient

Retry 429 and 503 with backoff. Treat 401 and 403 as permanent until the key is fixed:

python
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

bash
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 '')"
done

Build your own confluence: symbols on 3 or more boards

python
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

python
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

python
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 cache

9. Scope

What this is not:

  • No price history, candles or backfill. price, dayPct and volume are 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.*