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

| 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**

```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**

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

```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 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" }`:

```json
{ "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:

```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.*
