//TIADeveloper docs

Trade Ideas Automation

Powered by Mudrex

Project TIA API Documentation

A typed JSON REST API for consuming published trade ideas programmatically. Use /v1/trade-ideas/active for every LIVE signal (combine with entry_hit_at where you distinguish pre-entry vs in-trade), optional long poll for freshness, plus /closed, /history, /reports, and /v1/trade-ideas/{id}.

Base · https://project-tia.appAuth · Bearer + per-client IP

Overview

The Trade Ideas API is a JSON REST surface with five read endpoints:

  • GET /v1/trade-ideas/active — every trade idea with status LIVE and a non-null published_at (stop orders waiting for entry never appear until auto-published), newest published_at first.
  • GET /v1/trade-ideas/closed — recently terminal ideas (TP_HIT, TP1_HIT, TP2_HIT, SL_HIT, EXPIRED, WITHDRAWN) within the lookback window.
  • GET /v1/trade-ideas/history — full published archive (LIVE plus terminal rows), paginated.
  • GET /v1/trade-ideas/reports — aggregate performance metrics (win rate, ROI, hit counts) for a day / week / month / year or custom IST date window.
  • GET /v1/trade-ideas/{id} — one idea by id; a successful response is the trade idea JSON object at the root, not an envelope with data.

LIVE rows may still track without an entry stamp: use entry_hit_at, status, and hit timestamps in the JSON—not informal labels—to decide how you surface them in your own systems (Status values).

All live evaluation uses last traded price (LTP) from Bybit linear perpetual lastPrice (WebSocket plus REST reconcile) — not Mudrex, index, or mark. Entry, TP, and SL hits are stamped from those market prices on the tracker.

All responses are JSON; timestamps are ISO 8601 UTC instants on the wire (trailing Z). Numeric prices and percentages are strings so clients preserve precision without float parsing.

Specifications

  • Host: project-tia.app
  • Base path: /v1/trade-ideas
  • Example: https://project-tia.app/v1/trade-ideas/active
  • Headers: Authorization: Bearer <token> on every request; JSON responses use string numerics for precision.
  • Rate limits: 240 requests / minute per client IP (burst 120). See Rate limits.

Access

Access is layered: a per-partner bearer token (provided when your integration is onboarded), optional per-client IP allowlists, and always-on rate limits. Bearer tokens are required in production.

Bearer token (required)

Send your assigned token on every request:

Authorization: Bearer tia_live_…

Store the token as a secret environment variable (for example TIA_API_TOKEN on Railway) and attach it to your HTTP client — no URL or response-format changes are required.

export TIA_API_TOKEN="tia_live_…"
curl -sS -H "Authorization: Bearer $TIA_API_TOKEN" "https://project-tia.app/v1/trade-ideas/active"
Bearer required. Bearer tokens are required on every /v1/trade-ideas/* request in production. Send Authorization: Bearer with your assigned tia_live_… token; requests without a valid token receive HTTP 403.

Per-client IP allowlists

When IP allowlisting is required for your integration, requests that include your bearer token must originate from an IP or CIDR registered for your account. Multiple partners may share the same egress IP (for example Railway) and remain distinguishable because each has a unique token.

Need a static egress IP? Most cloud providers offer a NAT gateway or an Elastic IP. We accept individual hosts (203.0.113.42 → stored as /32) or CIDR ranges (203.0.113.0/24).

Deprecated (do not use)

  • Global IP-only allowlists — superseded by per-client IP lists used together with bearer tokens.
  • TIA_PUBLIC_API_BEARER_TOKEN environment variable — ignored; use the per-partner token provided at onboarding instead.
  • /v1/picks/* paths — redirect to /v1/trade-ideas/* with HTTP 308; migrate callers to the new paths.
  • API keys, scopes, and self-service token registration — not supported on this API.

Rate limits

Default quota: 240 requests per minute with a burst of 120. When you send a valid bearer token, limits apply per integration (not only per IP).

Prefer long polling on /v1/trade-ideas/active (see below)—one hanging request per update cycle uses fewer hits than spraying sub-second polls. Short interval polling remains fine under the quota.

When you exceed the limit you will receive an HTTP 429 with a Retry-After header (in seconds):

{
  "error": "rate_limited"
}

Status values

Every visible trade idea has exactly one wire status:

LIVE
Still monitored on the tracker—not terminal. Leaves GET /active on TP_HIT (new ideas), legacy TP2_HIT, SL_HIT, EXPIRED, or WITHDRAWN.
TP_HIT
Closed in profit at the full take-profit (new single-TP ideas). Same economic meaning as legacy TP2_HIT. TIA does not alias this as TP2_HIT.
TP2_HIT
Legacy dual-TP: closed at second take-profit.
TP1_HIT
Legacy dual-TP: closed at first take-profit or validity win after TP1.
SL_HIT
Closed at stop-loss.
EXPIRED
Validity window passed without a full-target win or SL_HIT.
WITHDRAWN
Removed from the tracker live set before TP/SL/EXPIRED—retained on GET /history and GET by id with withdrawn_at and resolved_at populated.
Important: LIVE alone does not guarantee the tracker has stamped entry yet. Use entry_hit_at: null means no entry stamp under publish-time rules yet; once set, milestones like tp1_hit_at proceed per the tracker.

For some ideas classified at publish time, take-profit / stop-loss / milestones are tracked by the tracker only after entry_hit_at becomes non-null. Until then,LIVE persists and later hit timestamps stay null even though price moves in the broader market.

expiry_at is the published validity cut-off. Operators may move it earlier or later on a LIVE idea (dashboard Amend expiry, up to 30 days from now); integrators should accept an extra trade_idea.updated whose expiry_at can be smaller or larger while the idea remains LIVE. If TP2 or SL triggers before then, status becomes terminal; otherwise EXPIRED. On leftover dual-TP ideas a touch of tp1_price records tp1_hit_at without changing status — the idea can remain LIVE. New single-TP ideas have one target: a full-target touch is TP_HIT and emits trade_idea.closed (the idea leaves GET /active).

Trade idea enrichment

For in-app widgets and partner overlays, map potential profit to est_potential_pct (leverage-adjusted move to the full take-profit at publish time — canonical tp_price). Two additive fields support deeplink and coin imagery:

  • mudrex_deeplink — opens Mudrex futures with published entry, SL, and TP1. Null without a Mudrex asset id.
  • logo_url — verified images.mudrex.com URL or a manual override. Null until probed OK or overridden.

Optional fields for in-app deeplinks and coin imagery. Both may be null until catalogue data is available — safe to ignore if your integration does not use them.

Single-TP migration

New publishes have one take-profit. REST and webhooks use the same payload for every partner. Production and staging both use the grace payload (compat tp1_* / tp2_* still present).

Grace (current default — leave the Admin toggle off): every idea includes canonical tp_price / tp_hit_at. New ideas also send tp1_price equal to tp_price and tp2_price: null. Leftover dual-TP rows keep stored tp1_price / tp2_price (do not assume tp1_price was rewritten to TP2). Full-target wins on new ideas use status: TP_HIT — not a fake TP2_HIT. Legacy dual-TP rows may still show TP1_HIT / TP2_HIT.
PhaseREST / webhooksIntegrator action
Grace (now)tp_* plus stored tp1_* / tp2_*. New ideas: tp1_price === tp_price, tp2_price null, status=TP_HIT on new wins. Dual-TP leftover: tp1_price stays TP1.Prefer tp_price / TP_HIT. If you still read tp1_price, accept tp2_price: null. Do not require a second target.
Enforced (later)tp_* only — tp1_* / tp2_* omittedMust use canonical fields. Operators flip Enforce single-TP API payload in Admin → Access after partners migrate.

Risk: a client that requires a numeric tp2_price or only treats TP2_HIT as a win will mis-handle new ideas even in grace.

Rollout: production and staging both publish single-TP with the grace payload (toggle off). Prefer tp_* / TP_HIT now; enforcement (drop tp1_* / tp2_*) comes later after partners confirm.

Grace-period example (new LIVE idea). Field order on the wire follows serialize; compat keys are present while the toggle is off:

{
  "id": "a1b2c3d4-1111-2222-3333-444444444444",
  "symbol": "BTCUSDT",
  "direction": "LONG",
  "margin_type": "ISOLATED",
  "leverage": 10,
  "entry_price": "62500.00000000",
  "stop_loss": "62000.00000000",
  "est_potential_pct": "16.0",
  "est_loss_pct": "-8.0",
  "risk_reward_ratio": "1:2",
  "confidence": "HIGH",
  "status": "LIVE",
  "published_at": "2026-09-03T06:00:00.000Z",
  "expiry_at": "2026-09-04T06:00:00.000Z",
  "entry_hit_at": null,
  "tp_price": "63500.00000000",
  "tp_hit_at": null,
  "sl_hit_at": null,
  "expired_at": null,
  "withdrawn_at": null,
  "resolved_at": null,
  "resolution": null,
  "mudrex_deeplink": "mudrex://futures-buy-sell/abc123/long/BTCUSDT/10/62500.00000000/62000.00000000/63500.00000000",
  "logo_url": "https://images.mudrex.com/BTC.png",
  "tp1_price": "63500.00000000",
  "tp2_price": null,
  "tp1_hit_at": null,
  "tp2_hit_at": null
}

Response shape

Every trade idea object exposes the fields below with this key order on the wire. Numeric prices and percentages are strings so callers can parse with decimal libraries without floats.

{
  "id": "8c2a9b7e-4f12-4d8a-9e3b-7a6e5b1c2d3f",
  "symbol": "BTCUSDT",
  "direction": "LONG",
  "margin_type": "ISOLATED",
  "leverage": 10,
  "entry_price": "62500.00000000",
  "stop_loss": "62000.00000000",
  "est_potential_pct": "32.0",
  "est_loss_pct": "-8.0",
  "risk_reward_ratio": "1:4",
  "confidence": "HIGH",
  "status": "LIVE",
  "published_at": "2026-05-12T13:42:01.000Z",
  "expiry_at": "2026-05-13T13:42:01.000Z",
  "entry_hit_at": null,
  "tp_price": "64500.00000000",
  "tp_hit_at": null,
  "sl_hit_at": null,
  "expired_at": null,
  "withdrawn_at": null,
  "resolved_at": null,
  "resolution": null,
  "mudrex_deeplink": "mudrex://futures-buy-sell/abc123/long/BTCUSDT/10/62500.00000000/62000.00000000/63500.00000000",
  "logo_url": "https://images.mudrex.com/BTC.png",
  "tp1_price": "63500.00000000",
  "tp2_price": "64500.00000000",
  "tp1_hit_at": null,
  "tp2_hit_at": null
}

Leftover dual-TP full-target close (TP2_HIT) — tp_price is TP2; tp1_price stays stored TP1:

{
  "id": "8c2a9b7e-4f12-4d8a-9e3b-7a6e5b1c2d3f",
  "symbol": "BTCUSDT",
  "direction": "LONG",
  "margin_type": "ISOLATED",
  "leverage": 10,
  "entry_price": "62500.00000000",
  "stop_loss": "62000.00000000",
  "est_potential_pct": "32.0",
  "est_loss_pct": "-8.0",
  "risk_reward_ratio": "1:4",
  "confidence": "HIGH",
  "status": "TP2_HIT",
  "published_at": "2026-05-12T13:42:01.000Z",
  "expiry_at": "2026-05-13T13:42:01.000Z",
  "entry_hit_at": "2026-05-12T13:58:02.000Z",
  "tp_price": "64500.00000000",
  "tp_hit_at": "2026-05-12T14:11:09.000Z",
  "sl_hit_at": null,
  "expired_at": null,
  "withdrawn_at": null,
  "resolved_at": "2026-05-12T14:11:09.000Z",
  "resolution": "TP2_HIT",
  "mudrex_deeplink": "mudrex://futures-buy-sell/abc123/long/BTCUSDT/10/62500.00000000/62000.00000000/63500.00000000",
  "logo_url": "https://images.mudrex.com/BTC.png",
  "tp1_price": "63500.00000000",
  "tp2_price": "64500.00000000",
  "tp1_hit_at": "2026-05-12T14:02:11.000Z",
  "tp2_hit_at": "2026-05-12T14:11:09.000Z"
}

Withdrawn trades use the same field order; terminal example WITHDRAWN:

{
  "id": "9d3bacc1-aaaa-bbbb-cccc-dddddddddddd",
  "symbol": "ETHUSDT",
  "direction": "SHORT",
  "margin_type": "ISOLATED",
  "leverage": 5,
  "entry_price": "3000.00000000",
  "stop_loss": "3050.00000000",
  "est_potential_pct": "25.0",
  "est_loss_pct": "-8.3",
  "risk_reward_ratio": "1:3",
  "confidence": "HIGH",
  "status": "WITHDRAWN",
  "published_at": "2026-05-14T09:00:00.000Z",
  "expiry_at": "2026-05-15T09:00:00.000Z",
  "entry_hit_at": null,
  "tp_price": "2850.00000000",
  "tp_hit_at": null,
  "sl_hit_at": null,
  "expired_at": null,
  "withdrawn_at": "2026-05-14T09:42:03.000Z",
  "resolved_at": "2026-05-14T09:42:03.000Z",
  "resolution": "WITHDRAWN",
  "mudrex_deeplink": "mudrex://futures-buy-sell/def456/short/ETHUSDT/5/3000.00000000/3050.00000000/2900.00000000",
  "logo_url": "https://images.mudrex.com/ETH.png",
  "tp1_price": "2900.00000000",
  "tp2_price": "2850.00000000",
  "tp1_hit_at": null,
  "tp2_hit_at": null
}
FieldTypeNotes
idstring (uuid)Stable identifier for this trade idea.
symbolstringTrading pair, e.g. BTCUSDT.
direction"LONG" | "SHORT"Side of the trade.
margin_type"ISOLATED"Always ISOLATED in this version.
leverageintegerLeverage multiplier carried on the idea (product rules cap per symbol; nominally up to 100× where supported).
entry_pricestring (decimal)Suggested entry price.
tp_pricestring (decimal)Canonical full take-profit level (always present on new ideas). Prefer this over tp1_price / tp2_price.
tp_hit_atISO 8601 UTC | nullWhen the full take-profit resolved; set when status is TP_HIT (new ideas) or legacy TP2_HIT / TP1_HIT.
tp1_pricestring (decimal)Grace-period stored first target. Equals tp_price on new single-TP ideas. Leftover dual-TP rows keep the original TP1 (not rewritten to TP2). Omitted when Admin enforces single-TP payload.
tp2_pricestring (decimal) | nullGrace-period compat: null on new single-TP ideas. Omitted when enforcement is on.
stop_lossstring (decimal)Stop-loss level.
est_potential_pctstring (decimal)Leverage-adjusted potential profit at full TP.
est_loss_pctstring (decimal)Leverage-adjusted stop-loss scenario (negative). Capped at −100.0 here to match isolated-margin loss floor in outbound JSON.
risk_reward_ratiostring ("1:R") | nullRisk : reward ratio in price units (e.g. 1:2.5 — risk 1, reward 2.5).
confidence"HIGH" | "MEDIUM" | "LOW"Confidence tier attached at publish time.
status"LIVE" | "TP_HIT" | "TP1_HIT" | "TP2_HIT" | "SL_HIT" | "EXPIRED" | "WITHDRAWN"LIVE until terminal — new wins use TP_HIT (full target). Legacy rows may show TP1_HIT / TP2_HIT. tp1_* / tp2_* fields drop when single-TP payload enforcement is enabled.
published_atISO 8601 UTC | nullInstant the idea entered the public feed. Rows returned by these endpoints include a timestamp when publishing has occurred.
expiry_atISO 8601 UTCValidity cut-off. While status is LIVE this can move earlier or later (Amend expiry: still in the future, at most 30 days from the change). The row stays on GET /active until the new cut-off and partners receive trade_idea.updated. expired_at is set only when status becomes EXPIRED. Distinct from WITHDRAWN.
entry_hit_atISO 8601 UTC | nullWhen linear LTP first satisfied entry rules versus publish-time classification; null means no entry stamp yet. TP/SL handling follows tracker rules afterward.
tp1_hit_atISO 8601 UTC | nullWhen LTP touched tp1_price. May be set while status is still LIVE.
tp2_hit_atISO 8601 UTC | nullSet when TP2 resolved and status becomes TP2_HIT.
sl_hit_atISO 8601 UTC | nullSet when stop-loss resolves and status becomes SL_HIT.
expired_atISO 8601 UTC | nullSet when expiry resolves and status becomes EXPIRED.
withdrawn_atISO 8601 UTC | nullSet when status is WITHDRAWN via operational withdrawal while LIVE—null otherwise.
resolved_atISO 8601 UTC | nullConvenience: null while LIVE; for terminal rows mirrors the closing event (tp1_hit_at for TP_HIT / clean TP1_HIT, sl_hit_at for TP1-then-SL, tp2_hit_at, expired_at, or withdrawn_at).
resolutionstring | nullTerminal outcome detail — e.g. TP_HIT, TP1_HIT_THEN_SL, TP2_HIT, SL_HIT. Null while LIVE.
mudrex_deeplinkstring | nullMudrex app deeplink with published entry, SL, and TP1. Null when Mudrex asset id is unknown.
logo_urlstring | nullVerified coin image URL (Mudrex CDN when available). Null when no image is available.
GET/v1/trade-ideas/active

Returns JSON { "data": [ … ] }: trade ideas currently LIVE (still tracked). Newest published_at first. When long polling (wait_ms with since_published_at), responses also wake when the LIVE-set membership changes—for example a row moving to WITHDRAWN—so removals from /active propagate without waiting full timeout.

Query parameters

NameTypeRequiredDescription
since_published_atISO 8601 UTCoptionalWatermark from your last snapshot: the max published_at among rows you processed. Ignored unless wait_ms > 0. When waiting, the connection is released when some LIVE trade idea has published_at STRICTLY AFTER this timestamp, or when wait_ms elapses.
wait_msint 0–28500optionalIf > 0, enables long polling (requires since_published_at). Typical value 28500 (~28.5 s). Backend polls periodically and returns when a newer LIVE published_at qualifies, when the LIVE id set gains or loses membership (withdraw or overlapping publish), or at deadline.

Request · curl

export TIA_API_TOKEN="tia_live_…"
curl -sS -H "Authorization: Bearer $TIA_API_TOKEN" "https://project-tia.app/v1/trade-ideas/active"
# Long poll (~28.5 s cap) until a fresher publish than your watermark:
curl -sS -H "Authorization: Bearer $TIA_API_TOKEN" "https://project-tia.app/v1/trade-ideas/active?wait_ms=28500&since_published_at=2026-05-15T12:00:00.000Z"

Request · JavaScript

const headers = { Authorization: `Bearer ${process.env.TIA_API_TOKEN}` };

// One-shot snapshot
let res = await fetch("https://project-tia.app/v1/trade-ideas/active", { headers });
let { data } = await res.json();

// Long poll loop: keep the latest published watermark
function maxPublished(rows) {
  return rows.reduce(
    (m, r) =>
      !m || Date.parse(r.published_at) > Date.parse(m) ? r.published_at : m,
    null
  );
}
let watermark = maxPublished(data) ?? "1970-01-01T00:00:00.000Z";
while (true) {
  res = await fetch(
    "https://project-tia.app/v1/trade-ideas/active?wait_ms=28500&since_published_at=" +
      encodeURIComponent(watermark),
    { headers }
  );
  ({ data } = await res.json());
  if (data.length) watermark = maxPublished(data) ?? watermark;
  // Diff data vs your local state — then reconnect with updated watermark.
}

200 OK response

{
  "data": [
    {
      "id": "8c2a9b7e-4f12-4d8a-9e3b-7a6e5b1c2d3f",
      "symbol": "BTCUSDT",
      "direction": "LONG",
      "margin_type": "ISOLATED",
      "leverage": 10,
      "entry_price": "62500.00000000",
      "stop_loss": "62000.00000000",
      "est_potential_pct": "32.0",
      "est_loss_pct": "-8.0",
      "risk_reward_ratio": "1:4",
      "confidence": "HIGH",
      "status": "LIVE",
      "published_at": "2026-05-12T13:42:01.000Z",
      "expiry_at": "2026-05-13T13:42:01.000Z",
      "entry_hit_at": null,
      "tp_price": "64500.00000000",
      "tp_hit_at": null,
      "sl_hit_at": null,
      "expired_at": null,
      "withdrawn_at": null,
      "resolved_at": null,
      "resolution": null,
      "mudrex_deeplink": "mudrex://futures-buy-sell/abc123/long/BTCUSDT/10/62500.00000000/62000.00000000/63500.00000000",
      "logo_url": "https://images.mudrex.com/BTC.png",
      "tp1_price": "63500.00000000",
      "tp2_price": "64500.00000000",
      "tp1_hit_at": null,
      "tp2_hit_at": null
    }
  ]
}
GET/v1/trade-ideas/closed

Returns JSON { data, since, limit }: terminal rows (TP_HIT, TP1_HIT, TP2_HIT, SL_HIT, EXPIRED, WITHDRAWN) matching the lookback cursor on tp1_hit_at, tp2_hit_at, sl_hit_at, expired_at, or withdrawn_at, newest closure first. Full trade idea objects in data. New single-TP wins stamp only tp1_hit_at (status TP_HIT) — they are included.

Query parameters

NameTypeRequiredDescription
sinceISO 8601 UTCoptionalLookback start. Defaults to 24 hours before the server clock at request time.
limitint 1–100optionalMaximum rows. Defaults to 100.

Request · curl

export TIA_API_TOKEN="tia_live_…"
curl -sS -H "Authorization: Bearer $TIA_API_TOKEN" "https://project-tia.app/v1/trade-ideas/closed?since=2026-05-12T00:00:00Z&limit=50"

Request · JavaScript

const headers = { Authorization: `Bearer ${process.env.TIA_API_TOKEN}` };
const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
const res = await fetch(`https://project-tia.app/v1/trade-ideas/closed?since=${since}`, { headers });
const { data, since: windowStart, limit } = await res.json();

200 OK response

{
  "since": "2026-05-12T00:00:00.000Z",
  "limit": 50,
          "data": [
    {
      "id": "a1b2c3d4-1111-2222-3333-444444444444",
      "symbol": "BTCUSDT",
      "direction": "LONG",
      "margin_type": "ISOLATED",
      "leverage": 10,
      "entry_price": "62500.00000000",
      "stop_loss": "62000.00000000",
      "est_potential_pct": "16.0",
      "est_loss_pct": "-8.0",
      "risk_reward_ratio": "1:2",
      "confidence": "HIGH",
      "status": "TP_HIT",
      "published_at": "2026-09-03T06:00:00.000Z",
      "expiry_at": "2026-09-04T06:00:00.000Z",
      "entry_hit_at": "2026-09-03T06:01:00.000Z",
      "tp_price": "63500.00000000",
      "tp_hit_at": "2026-09-03T06:12:00.000Z",
      "sl_hit_at": null,
      "expired_at": null,
      "withdrawn_at": null,
      "resolved_at": "2026-09-03T06:12:00.000Z",
      "resolution": "TP_HIT",
      "mudrex_deeplink": "mudrex://futures-buy-sell/abc123/long/BTCUSDT/10/62500.00000000/62000.00000000/63500.00000000",
      "logo_url": "https://images.mudrex.com/BTC.png",
      "tp1_price": "63500.00000000",
      "tp2_price": null,
      "tp1_hit_at": "2026-09-03T06:12:00.000Z",
      "tp2_hit_at": null
    }
          ]
}
GET/v1/trade-ideas/history

Returns JSON { data, paging }: every published idea with status LIVE, TP_HIT, TP1_HIT, TP2_HIT, SL_HIT, EXPIRED, or WITHDRAWN (rejected / pending never appear), newest published_at first. paging.has_more and paging.next_cursor continue cursor-based pagination.

Query parameters

NameTypeRequiredDescription
fromISO 8601 UTCoptionalFilter: only ideas published at or after this timestamp.
toISO 8601 UTCoptionalFilter: only ideas published at or before this timestamp.
limitint 1–200optionalPage size. Defaults to 50.
cursorid (uuid)optionalOpaque page cursor — pass paging.next_cursor from the previous response to fetch the next page.

Request · curl

export TIA_API_TOKEN="tia_live_…"
curl -sS -H "Authorization: Bearer $TIA_API_TOKEN" "https://project-tia.app/v1/trade-ideas/history?limit=50"

Request · JavaScript

const headers = { Authorization: `Bearer ${process.env.TIA_API_TOKEN}` };
let cursor = null;
do {
  const url = new URL("https://project-tia.app/v1/trade-ideas/history");
  url.searchParams.set("limit", "100");
  if (cursor) url.searchParams.set("cursor", cursor);
  const { data, paging } = await (await fetch(url, { headers })).json();
  for (const idea of data) console.log(idea.id, idea.status);
  cursor = paging.has_more ? paging.next_cursor : null;
} while (cursor);

200 OK response

{
  "data": [
    {
      "id": "8c2a9b7e-4f12-4d8a-9e3b-7a6e5b1c2d3f",
      "symbol": "BTCUSDT",
      "direction": "LONG",
      "margin_type": "ISOLATED",
      "leverage": 10,
      "entry_price": "62500.00000000",
      "stop_loss": "62000.00000000",
      "est_potential_pct": "32.0",
      "est_loss_pct": "-8.0",
      "risk_reward_ratio": "1:4",
      "confidence": "HIGH",
      "status": "LIVE",
      "published_at": "2026-05-12T13:42:01.000Z",
      "expiry_at": "2026-05-13T13:42:01.000Z",
      "entry_hit_at": null,
      "tp_price": "64500.00000000",
      "tp_hit_at": null,
      "sl_hit_at": null,
      "expired_at": null,
      "withdrawn_at": null,
      "resolved_at": null,
      "resolution": null,
      "mudrex_deeplink": "mudrex://futures-buy-sell/abc123/long/BTCUSDT/10/62500.00000000/62000.00000000/63500.00000000",
      "logo_url": "https://images.mudrex.com/BTC.png",
      "tp1_price": "63500.00000000",
      "tp2_price": "64500.00000000",
      "tp1_hit_at": null,
      "tp2_hit_at": null
    }
  ],
  "paging": {
    "limit": 50,
    "has_more": true,
    "next_cursor": "8c2a9b7e-4f12-4d8a-9e3b-7a6e5b1c2d3f"
  }
}
GET/v1/trade-ideas/reports

Returns JSON { "data": { … } }: aggregate performance metrics for trade ideas whose published_at falls in the selected window. Win rate uses the tp_any rule (TP_HIT, TP1_HIT, or TP2_HIT counts as a win; denominator is TP/SL hits only). tp2_hits is the full-target bucket (TP2_HIT and single-TP TP_HIT, counted once — not also in tp1_hits). Margin ROI uses the same realized est_*_pct formulas as the internal desk. Optional simulation query params scale results to fixed account risk % or fixed USDT margin per trade — see roi_simulations in the response. active_trades is a global snapshot of every LIVE idea at request time (not period-filtered).

Query parameters

NameTypeRequiredDescription
periodday | week | month | year | customoptionalReporting window. Defaults to month (last 30 days, rolling). day = last 24h; week = last 7d; year = last 365d.
fromYYYY-MM-DDoptionalRequired when period=custom. Start of IST calendar day (inclusive).
toYYYY-MM-DDoptionalOptional when period=custom. End of IST calendar day (inclusive). Defaults to now when omitted.
risk_pctnumberoptionalAccount % risked per full SL for fixed-risk ROI simulations. Default 1 (0.01–100).
fixed_margin_usdtnumberoptionalUSDT margin per trade for fixed-margin PnL simulation. Default 500.
starting_account_usdtnumberoptionalStarting account balance for fixed-margin account ROI %. Default 10000.

Request · curl

export TIA_API_TOKEN="tia_live_…"
curl -sS -H "Authorization: Bearer $TIA_API_TOKEN" "https://project-tia.app/v1/trade-ideas/reports"
curl -sS -H "Authorization: Bearer $TIA_API_TOKEN" "https://project-tia.app/v1/trade-ideas/reports?period=week"
curl -sS -H "Authorization: Bearer $TIA_API_TOKEN" "https://project-tia.app/v1/trade-ideas/reports?period=month&risk_pct=1&fixed_margin_usdt=500&starting_account_usdt=10000"
curl -sS -H "Authorization: Bearer $TIA_API_TOKEN" "https://project-tia.app/v1/trade-ideas/reports?period=custom&from=2026-05-01&to=2026-05-31"

Request · JavaScript

const headers = { Authorization: `Bearer ${process.env.TIA_API_TOKEN}` };

const res = await fetch("https://project-tia.app/v1/trade-ideas/reports?period=month&risk_pct=1", { headers });
const { data } = await res.json();
console.log(data.cumulative_roi_pct, data.roi_simulations.fixed_risk_compounded_account_roi_pct);
console.log(data.active_trades.total, "LIVE now");

200 OK response

{
  "data": {
    "period": {
      "kind": "month",
      "from": "2026-05-06T18:30:00.000Z",
      "to": "2026-06-05T09:00:00.000Z",
      "label": "Last 30 days",
      "timezone": "Asia/Kolkata"
    },
    "win_definition": "tp_any",
    "published_count": 42,
    "win_rate_pct": "65.0",
    "win_rate_eligible": 30,
    "wins": 20,
    "losses": 10,
    "cumulative_roi_pct": "12.5",
    "average_roi_pct": "0.42",
    "average_rr_ratio": "1:2",
    "roi_simulations": {
      "sum_margin_roi_pct": "12.5",
      "fixed_risk_arithmetic_account_roi_pct": "4.20",
      "fixed_risk_compounded_account_roi_pct": "4.35",
      "fixed_margin_total_pnl_usdt": "62.50",
      "fixed_margin_account_roi_pct": "0.63",
      "assumptions": {
        "risk_pct": "1",
        "fixed_margin_usdt": "500",
        "starting_account_usdt": "10000"
      }
    },
    "tp1_hits": 18,
    "tp2_hits": 12,
    "sl_hits": 10,
    "expired_count": 5,
    "withdrawn_count": 2,
    "open_in_period": 3,
    "active_trades": {
      "total": 4,
      "awaiting_entry": 1,
      "in_progress": 3
    }
  }
}
GET/v1/trade-ideas/{id}

Returns HTTP 200 with the trade idea JSON object at the response root (same keys as elsewhere). Pending-review and rejected picks return HTTP 404 with { "error": "not_found" }.

Request · curl

export TIA_API_TOKEN="tia_live_…"
curl -sS -H "Authorization: Bearer $TIA_API_TOKEN" "https://project-tia.app/v1/trade-ideas/8c2a9b7e-4f12-4d8a-9e3b-7a6e5b1c2d3f"

Request · JavaScript

const headers = { Authorization: `Bearer ${process.env.TIA_API_TOKEN}` };
const id = "8c2a9b7e-4f12-4d8a-9e3b-7a6e5b1c2d3f";
const res = await fetch(`https://project-tia.app/v1/trade-ideas/${id}`, { headers });
if (res.status === 404) {
  console.log("not found");
} else {
  const idea = await res.json();
  console.log(idea);
}

200 OK response

Successful responses are not wrapped in data — the body is the serialized trade idea.

{
  "id": "8c2a9b7e-4f12-4d8a-9e3b-7a6e5b1c2d3f",
  "symbol": "BTCUSDT",
  "direction": "LONG",
  "margin_type": "ISOLATED",
  "leverage": 10,
  "entry_price": "62500.00000000",
  "stop_loss": "62000.00000000",
  "est_potential_pct": "32.0",
  "est_loss_pct": "-8.0",
  "risk_reward_ratio": "1:4",
  "confidence": "HIGH",
  "status": "LIVE",
  "published_at": "2026-05-12T13:42:01.000Z",
  "expiry_at": "2026-05-13T13:42:01.000Z",
  "entry_hit_at": null,
  "tp_price": "64500.00000000",
  "tp_hit_at": null,
  "sl_hit_at": null,
  "expired_at": null,
  "withdrawn_at": null,
  "resolved_at": null,
  "resolution": null,
  "mudrex_deeplink": "mudrex://futures-buy-sell/abc123/long/BTCUSDT/10/62500.00000000/62000.00000000/63500.00000000",
  "logo_url": "https://images.mudrex.com/BTC.png",
  "tp1_price": "63500.00000000",
  "tp2_price": "64500.00000000",
  "tp1_hit_at": null,
  "tp2_hit_at": null
}

Outbound webhooks (optional integration)

Integrations may receive signed HTTPS POST payloads on URLs provisioned for your environment—an alternative to polling /v1/trade-ideas/active. Delivery starts when a trade idea is published (LIVE), continues on materially relevant live updates (entry / TP1 progress, terminals), and uses the same snake_case trade_idea object as REST. Limit orders waiting for a fill show LIVE with entry_hit_at: null exactly like REST.

  • Headers include Content-Type: application/json, X-TIA-Timestamp (Unix milliseconds), and X-TIA-Signature: sha256=<hex digest> where digest is HMAC-SHA256 over {timestamp}.{rawBody} (ASCII dot between the two). Optional X-TIA-Webhook-Event-Group-Id matches envelope event_id; X-TIA-Webhook-Event-Type mirrors envelope event_type.
  • Lifecycle events: trade_idea.published (first publish), trade_idea.updated (entry or TP1 progress, or validity moved earlier or later while LIVE), trade_idea.closed (TP_HIT, legacy TP2/TP1, SL, expiry, or withdrawal). Connectivity checks may send integration.ping.
{
  "schema_version": 1,
  "event_id": "8c2a9b7e-4f12-4d8a-9e3b-7a6e5b1c2d3f",
  "event_type": "trade_idea.published",
  "occurred_at": "2026-05-12T13:42:01.000Z",
  "trade_idea":   {
    "id": "8c2a9b7e-4f12-4d8a-9e3b-7a6e5b1c2d3f",
    "symbol": "BTCUSDT",
    "direction": "LONG",
    "margin_type": "ISOLATED",
    "leverage": 10,
    "entry_price": "62500.00000000",
    "stop_loss": "62000.00000000",
    "est_potential_pct": "32.0",
    "est_loss_pct": "-8.0",
    "risk_reward_ratio": "1:4",
    "confidence": "HIGH",
    "status": "LIVE",
    "published_at": "2026-05-12T13:42:01.000Z",
    "expiry_at": "2026-05-13T13:42:01.000Z",
    "entry_hit_at": null,
    "tp_price": "64500.00000000",
    "tp_hit_at": null,
    "sl_hit_at": null,
    "expired_at": null,
    "withdrawn_at": null,
    "resolved_at": null,
    "resolution": null,
    "mudrex_deeplink": "mudrex://futures-buy-sell/abc123/long/BTCUSDT/10/62500.00000000/62000.00000000/63500.00000000",
    "logo_url": "https://images.mudrex.com/BTC.png",
    "tp1_price": "63500.00000000",
    "tp2_price": "64500.00000000",
    "tp1_hit_at": null,
    "tp2_hit_at": null
  }
}

Verify timestamps within a reasonable clock skew window; tolerate at-least-once deliveries by deduplicating on event_id.

{
  "schema_version": 1,
  "event_id": "9f2c…",
  "event_type": "integration.ping",
  "occurred_at": "2026-05-12T13:43:01.000Z",
  "message": "Handshake test from Project TIA."
}

Error responses

Errors return JSON with at least an error code. Validation responses from route handlers usually include message with a plain-language hint; clients should not depend on the exact copy of message.

HTTPError codeMeaning
400invalid_since`since` is not a valid ISO 8601 timestamp.
400invalid_period`period` is not day, week, month, year, or custom (GET /reports).
400invalid_sim_param`risk_pct`, `fixed_margin_usdt`, or `starting_account_usdt` is missing or out of range on GET /reports.
400invalid_fromInvalid `from` — ISO 8601 on GET /history; YYYY-MM-DD (IST) on GET /reports when period=custom.
400invalid_toInvalid `to` — ISO 8601 on GET /history; YYYY-MM-DD (IST) on GET /reports when period=custom.
400invalid_since_published_at`since_published_at` is not a valid ISO timestamp (only used with active long polling).
400missing_since_for_wait`wait_ms` is set but `since_published_at` is missing on GET /active.
403forbiddenBearer token required and missing, invalid, or disabled client.
403ip_not_allowedClient IP lists are enforced; your caller IP is not on your client's whitelist, or the server could not determine a client IP (requires a valid bearer token when lists are on).
404not_found{ id } lookups: idea missing or not public (pending / rejected); body is `{ "error": "not_found" }`.
429rate_limitedToo many requests for your client or IP bucket. Respect the Retry-After header.
503temporarily_unavailableDatabase connection pool timed out (Prisma P2024) or the route could not complete under load. Retry with backoff; long-polls on GET /active are the usual trigger.

Changelog

Breaking changes are called out explicitly. Integration requirements are in Access.

2026-09-23

  • Validity may be extended or shortened on LIVE ideas. Dashboard Amend expiry replaces early expire. expiry_at can move earlier or later (still in the future, at most 30 days from now). The trade idea stays on GET /v1/trade-ideas/active until the new cut-off. Integrators should handle trade_idea.updated when expiry_at changes in either direction. Terminal closure at validity is unchanged.

2026-09-03

  • Single-TP grace payload (staging rehearsal). New ideas publish one take-profit. While Enforce single-TP API payload is off, REST and webhooks send canonical tp_price / tp_hit_at and status=TP_HIT on new full-target wins. Compat tp1_price / tp2_price are stored levels (new ideas: tp1_price === tp_price, tp2_price: null; leftover dual-TP: original TP1/TP2). GET /closed lookback includes tp1_hit_at so TP_HIT rows appear. TIA does not alias TP_HIT as TP2_HIT. See Single-TP migration.

2026-06-23

  • Default rate limits raised. Token-bucket quota for all /v1/trade-ideas/* endpoints is now 240 requests per minute sustained with a burst of 120 (previously 120/min, burst 60). When you send a valid bearer token, limits apply per integration, not only per IP. Operators may override via TIA_PUBLIC_API_RL_PER_MIN and TIA_PUBLIC_API_RL_BURST. See Authentication & access.

2026-06-20

  • GET /v1/trade-ideas/reports — multi-metric ROI simulations. Non-breaking addition to the existing reports payload. cumulative_roi_pct is unchanged (sum of per-trade margin ROI %). Top-level average_roi_pct is the mean per win/loss trade with non-zero margin (not inside roi_simulations). New optional query params risk_pct (default 1), fixed_margin_usdt (default 500), and starting_account_usdt (default 10000) drive the roi_simulations block:sum_margin_roi_pct, fixed_risk_arithmetic_account_roi_pct, fixed_risk_compounded_account_roi_pct, fixed_margin_total_pnl_usdt, and fixed_margin_account_roi_pct (plus assumptions). Invalid simulation params return 400 with invalid_sim_param. See GET /reports.

2026-06-10

  • Bearer tokens mandatory in production. Bearer tokens are required on every /v1/trade-ideas/* request in production. Send Authorization: Bearer with your assigned tia_live_… token; requests without a valid token receive HTTP 403.The optional-token grace period has ended.

2026-06-09

  • GET /v1/trade-ideas/reports — aggregate performance metrics for partners: published count, win rate (tp_any — TP1 or TP2 is a win), cumulative and average ROI, average risk-reward ratio, TP1/TP2/SL/expired/withdrawn hit counts, and a global active_trades snapshot (all LIVE ideas at request time). Query period (day, week, month, year, custom) with optional IST from / to dates for custom windows. Non-breaking addition; existing endpoints unchanged.

2026-05-31

  • Validity may be shortened on LIVE ideas. expiry_at can be moved earlier before the original cut-off. The trade idea stays on GET /v1/trade-ideas/active until the new expiry_at; integrators should handle an extra trade_idea.updated with the smaller timestamp. Terminal closure at validity is unchanged — EXPIRED or TP1_HIT per existing rules at the new cut-off (distinct from WITHDRAWN). Superseded 2026-09-23: expiry_at may also move later. Do not treat updates as shorten-only.

2026-05-28

  • Per-partner bearer tokens. Each integration receives a unique tia_live_… token at onboarding. Send Authorization: Bearer … on every /v1/trade-ideas/* request. Store the token in a secret environment variable (for example TIA_API_TOKEN). When a valid token is present, rate limits apply per integration, not only per IP — so multiple partners on a shared Railway egress IP each get their own quota.
  • Per-client IP allowlists replace the former global IP-only model. When IP allowlisting is required, your token must be sent together with a caller IP registered for your account. Partners sharing one egress IP remain distinguishable by bearer token.
  • Historical — bearer grace period (ended June 2026). Tokens were optional during onboarding; production now requires a valid per-partner bearer on every request.
  • New / updated error codes: 403 forbidden (missing, invalid, or disabled bearer when tokens are required); 403 ip_not_allowed(caller IP not on your client's whitelist when IP lists are enforced). See Error responses.
  • Deprecated: global IP-only allowlists; server env TIA_PUBLIC_API_BEARER_TOKEN (use your assigned per-partner token instead); /v1/picks/* (308 redirect — use /v1/trade-ideas/*); API keys and self-service token registration.

2026-05-19

  • Non-breaking additions on every trade_idea object (REST and webhooks): mudrex_deeplink (string | null) — Mudrex app deeplink built from published entry_price, stop_loss, and tp1_price when a catalogue asset id exists; logo_url (string | null) — verified coin image URL from the Mudrex CDN or a manual override. Existing integrations may ignore both keys; no changes to data envelopes or existing field names.

2026-05-18

  • Outbound webhooks (optional): signed POST to your HTTPS listener with schema version 1 envelopes. Events trade_idea.published, trade_idea.updated, and trade_idea.closed carry the same trade_idea fields as REST (including entry_hit_at: null while a limit order awaits entry). Verify X-TIA-Signature over {timestamp}.{rawBody} and deduplicate on event_id (at-least-once delivery). See Webhooks.
  • est_loss_pct is floored at -100.0 when leverage-adjusted maths would imply a deeper loss—reflects isolated-margin cap in serialized responses.

2026-05-17

  • WITHDRAWN status and withdrawn_at: a LIVE trade idea may be ended without a TP/SL/expiry resolution. Rows leave /v1/trade-ideas/active but remain on /history and GET /v1/trade-ideas/{id} as terminal payloads. Long polling on /activewakes on LIVE-set membership changes (including withdrawal) ahead of wait_ms timeouts.

2026-05-16

  • Reference clarifications (no breaking change): interpreting LIVE with entry_hit_at, observing tp1_hit_at while status stays LIVE, and using linear perpetual LTP for hit logic.

2026-05-13

  • v1: public URLs under /v1/trade-ideas/*. Legacy /v1/picks/* redirects with 308 — migrate to /v1/trade-ideas/*. Initial release used IP-based access only; per-partner bearer tokens and per-client IP lists were added 2026-05-28 (see that date above).
  • GET /active supports optional wait_ms + since_published_at long polling for near-real-time delivery without tight polling loops.