# Stryke API — Full Reference > Market intelligence and execution across 74 chains with live market data — plus holder and security analysis on 29, and non-custodial quotes on 59. - **Base URL:** `https://api.stryke.gg` - **Auth:** the v1 trading & account API (`/v1/*`) authenticates with `Authorization: Bearer `; the v2 market-data API (`/api/v2/*`) uses `X-API-Key: `. Send it on every non-public request. Keys are issued per app/tenant and carry scopes (read, trade, wallet, automation, admin). - **Responses:** `{"success":true,...}` on success; `{"success":false,"error":"..."}` with HTTP 4xx/5xx on failure. `401` = missing/invalid key, `429` = rate limited. - **Endpoints:** 318 - **Chains:** 118 recognised — 74 with a live market tape, 29 with holder + security analysis, 59 quotable. Measured, not declared (last 2026-10-04T04:17:00Z). Read `GET https://api.stryke.gg/api/v2/chains` (no API key) rather than hard-coding a list; the chain-generic surface is `/api/v2/chain/{chain}/*`. ## Tokens Token data endpoints under /api/v2/tokens. Read-through layer over the market-data layer (market details, price history, recent trades, wallet labels), the metadata service (token metadata, largest accounts), the DEX index (pairs, search, boosts/trending, embeddable charts), and Stryke's on-chain indexer, fronted by an in-process memo cache (Stryke's engine). Also exposes two heavy on-chain analysis engines: Deep Scan (Stryke's engine — a 0-100 higher=safer rug/safety verdict folding ~16 own on-chain layers plus multiple external safety providers) and the Bubble Map entity graph (Stryke's engine — weighted union-find holder/funder graph). All endpoints require the X-API-Key header (validateApiKey) and count against the per-key rate limit (apiLimiter); only the sibling /api/v2/health route is unauthenticated. Mints are validated against base58 regex ^[1-9A-HJ-NP-Za-km-z]{32,44}$. Every successful response is JSON with a top-level success:true; handler errors return success:false with an error string (most upstream failures surface as HTTP 502). ### POST /api/v2/tokens/metadata **Batch token metadata** — Returns enriched metadata (name/symbol/image/decimals/supply/ownership) for up to 100 SPL mints in one call. Lookup chain: in-process cache -> local the on-chain indexer -> the metadata service for indexer misses. `POST https://api.stryke.gg/api/v2/tokens/metadata` · auth: `X-API-Key` **Body parameters:** - `mints` (string[], required) — Array of SPL mint addresses (base58). Invalid mints are filtered out and the list is capped to the first 100 valid mints. If zero valid mints, 400 'mints[] required (max 100 valid SPL mints)'. **Response:** tokens is a map keyed by mint. count = number of resolved mints (Object.keys(tokens).length); mints not found anywhere are simply absent. Each entry: mint, name, symbol, description, image, decimals, supply, ownership, source. source is 'das' for the metadata service-resolved entries (image populated from content.links.image / files cdn_uri), or 'indexer:' for fast-path rows from the local token indexer (description and image always null, plus an extra uri field). Indexer rows are only used when BOTH name and symbol are populated; otherwise the mint falls through to DAS. If Stryke's node layer is not configured, indexer-covered mints are still returned and the rest are absent. > Cache key per mint is das:, TTL 5 min. Indexer fast-path only runs when the indexer.configured() (the platform config set). DAS batch only runs when node.configured(). PARTIAL SET: if the metadata service is unavailable, only cache + indexer-covered mints are returned (HTTP 200, partial) — unresolved mints are silently absent. Invalid mints are filtered and the input is capped to the first 100. Indexer fast-path entries carry source in the form 'indexer:' and a null image. ### POST /api/v2/tokens/prices **Batch token prices** — Current price + core market stats for up to 50 mints in a single call. Each mint is fetched (and cached) independently, so a partial set still returns. Cached 30s per mint. `POST https://api.stryke.gg/api/v2/tokens/prices` · auth: `X-API-Key` **Body parameters:** - `mints` (string[], required) — Array of SPL mint addresses (base58). Invalid mints are filtered out and the list is capped to the first 50 valid mints. 400 'mints[] required (max 50 valid SPL mints)' if zero valid mints. **Response:** prices is an object keyed by mint → { mint, priceUsd, marketCap, fdv (fully-diluted valuation), liquidityUsd, volume24h, priceChange24h (24h %) }. count is the number of resolved entries. Mints that fail to resolve upstream are silently omitted from prices (the rest still return); compare the returned keys against your request to detect misses. > Per-mint cache key price:, TTL 30s — repeated batches that overlap are nearly free. Up to 50 mints are fanned out in parallel. JSON request body. PARTIAL MAP: mints that fail upstream are silently dropped — count reflects only resolved entries; diff your requested mints against the returned keys. Cached ~30s per mint. ### GET /api/v2/tokens/trending **Trending tokens feed** — Accurate, self-policing multi-window trending from the official the DEX index REST API (boosts/profiles seed + live pair enrichment). Boost-rank primary, h24-volume tiebreak. The FULL ranked set is memoized 60s under a param-independent key; dex/minLiquidityUsd/limit are applied per-request. Carries a freshness stamp. `GET https://api.stryke.gg/api/v2/tokens/trending` · auth: `X-API-Key` **Response:** Top-level: success, generatedAt (ms), maxAgeMs:60000, source:'aggregate', count (after filter/slice), tokens[]. Each token: mint, symbol, name, image, priceUsd, priceChange{m5,h1,h6,h24} (m5 often null), volume{m5,h1,h6,h24}, txns{m5:{buys,sells},h1,h6,h24}, liquidityUsd, marketCap, fdv, pairCreatedAt, dex (dexId), pairAddress, labels[], boosts, rank, rankChange1h. rank is gapless over the FULL set (dex/minLiquidity filtering may leave gaps); rankChange1h is null until ≥1h of history. This is a RICHER shape than /tokens/search. VENDOR-NEUTRAL (source:'aggregate'; vendor-CDN image hosts are dropped to null). > Cache key 'trending', TTL 60s — shared across all callers. NOTE: route order — this is declared AFTER /:mint/intel etc., but Express matches the literal '/trending' segment as a path param too; '/search' and '/trending' are literal routes and win because :mint variants validate the mint regex (handlers 400 on a non-mint, so 'trending'/'search' as a :mint would fail isMint). In practice GET /api/v2/tokens/trending hits this handler. SHARED SNAPSHOT: one shared ~60s cache for ALL callers (not per-query) — every consumer gets the same snapshot; the candidate set is capped to ~60 mints before enrichment. ### GET /api/v2/tokens/{mint}/intel **Token intel (safety verdict + holders + risk)** — Combines market details (holders, supply, top10/insider %), the DEX index, and Stryke-resolved top-10 owners with entity labels into a heuristic 0+ risk score and verdict. Cached 60s. `GET https://api.stryke.gg/api/v2/tokens/{mint}/intel` · auth: `X-API-Key` **Path parameters:** - `mint` (string, optional) — SPL token mint address (base58). 400 'invalid mint' if it fails the base58 regex. **Response:** Top-level (spread from cached data object): mint, name, symbol, image, price_usd, market_cap, fdv, liquidity_usd, volume_24h, supply (decimals-adjusted UI supply, decimals-aware basis preferring on-chain getTokenSupply), holders, holders_meta{value,source('provider'|'enumeration'),exact,capped,as_of}, holders_nonvault (stricter clean/non-dust count), top10_pct, insider_pct (both clamped <=100), top_holders[], taxonomies{entityType,flags}, risk{score,verdict,risks[]}, pair, generatedAt(ms). holders is NEVER a bare null when we enumerated (capped:true = page-capped lower bound -> render 'N+'). top_holders entries: owner, amount, pct (clamped <=100), accounts, entity, entityType (cex/liquidity/program/wallet/...), entity_source (provider|registry|pda|program), is_contract (true for LP/pool/program/bridge vaults — suppress 'whale'), flags[] (v2 normalized enum dev/sniper/bundler/insider/pro_trader/fresh_wallet/smart_money/whale by default; flags_taxonomy=raw returns legacy strings). risk.score is additive heuristic (top10>70 +30 / >50 +15; insiders>10 +25; bundler>15% +20; sniper>10% +10; dev>5% +15); risk.verdict = high-risk(>=50)/risky(>=25)/caution(>=10)/safe. No data-vendor name in the body (entity_source uses provider|registry|pda|program). Also returned: metadata{logo=square token icon, header=wide social/cover banner image, description, websites[], socials[], createdAt, deployer, bonded}, market, distribution, pairs[], rugScore, verdict, flags[], deepscanAvailable, checkedAt. > Cache key intel:, TTL 60s. Each upstream (market details, the DEX index, Stryke's node layer largest accounts + getMultipleAccounts owner resolution, the market-data layer wallet labels) is wrapped in.catch(()=>null) so the endpoint degrades gracefully — fields go null/empty rather than failing. Top-25 token accounts resolved -> merged by owner -> top 10 returned. DEGRADED RESPONSE: each data source is fetched independently and failures degrade field-by-field to null/empty rather than erroring — a success:true response can have null name/price and an empty top_holders. On the unlabeled path (entity data unavailable, or no resolvable owners) each top_holders entry omits `entityType` and has `entity:null`; do not assume entityType is always present. ### GET /api/v2/tokens/{mint}/ath **All-time high / low** — All-time-high and all-time-low USD price for a token, with the current price's signed % distance from each. Reads the market-data layer's native athUSD/atlUSD/athDate fields; if the market-data layer lacks them, derives ATH/ATL from the full price-history series. Cached 120s. `GET https://api.stryke.gg/api/v2/tokens/{mint}/ath` · auth: `X-API-Key` **Path parameters:** - `mint` (string, optional) — SPL token mint address (base58). 400 'invalid mint' if it fails ^[1-9A-HJ-NP-Za-km-z]{32,44}$. **Response:** price (current USD), ath / atl (all-time high/low USD), athDate / atlDate (ISO 8601 timestamps), fromAthPct / fromAtlPct (signed % distance of the current price from ATH/ATL — fromAthPct is ≤0, fromAtlPct ≥0), priceChange24h (24h % change). Values are all-time (full history), not windowed. When the native ATH/ATL is missing the fields are derived from the complete price series and the dates are emitted as ISO strings. > Cache key ath:, TTL 120s. One market-details call in the common case; a second price-history call only when ATH/ATL must be derived. fromAthPct/fromAtlPct are null unless BOTH the current price and the respective ATH/ATL are present and non-zero. ### GET /api/v2/tokens/{mint}/trades **Recent trades (live tape)** — Recent swaps on a mint's primary pair from the market-data layer, normalised into a compact trade-tape shape. Cached 30s per (mint,limit). `GET https://api.stryke.gg/api/v2/tokens/{mint}/trades` · auth: `X-API-Key` **Path parameters:** - `mint` (string, optional) — SPL token mint address (base58). 400 'invalid mint' if invalid. **Query parameters:** - `limit` (integer, optional, default `20`) — Number of trades. Number(limit)||20, clamped to [1,100]. **Response:** Top-level: mint, count (trades.length), trades[]. Each trade: hash (t.hash||t.transaction_hash), date, amountUSD (Number of token_amount_usd||amount_usd, 0 if absent), priceUSD (token_price||price_usd_token0||price_usd_token1), amount (token_amount), side (lowercased type/side e.g. 'buy'/'sell'), sender (sender||transaction_sender_address), platform. Returns 503 (success:false, 'trades provider not configured') when the market-data layer key is absent. > Cache key trades::, TTL 30s. the market-data layer upstream (market/trades/pair) is known to intermittently 503; here it surfaces as a 502 wrapper since getRecentTrades throws on non-ok. amountUSD/priceUSD/amount default to 0 when missing or unparseable — a 0 can mean genuinely zero OR unavailable; limit clamped [1,100]. ### GET /api/v2/tokens/{mint}/deepscan **Deep Scan (token safety / rug verdict)** — Runs the full Stryke Deep Scan engine: ~16 own on-chain layers (contract/holders/liquidity/bundle/creator/market/launch/holderstats/cohorts/lplock/honeypot/swapstats/devforensics/sniperforensics/programs/bubblemap) cross-validated by an external safety provider (always) plus an external safety provider/an external market provider/the holder-graph provider/an external market provider (when keyed), folded into one 0-100 higher=safer score, grade, verdict and flagged risks. Cached 120s. `GET https://api.stryke.gg/api/v2/tokens/{mint}/deepscan` · auth: `X-API-Key` **Path parameters:** - `mint` (string, optional) — SPL token mint address (base58). 400 'invalid mint' if invalid. Canonicalized (trimmed) internally. **Response:** Top-level (spread from deepScan result): mint (canonical), score (0-100, higher=safer), grade ('safe'>=85 | 'caution'>=60 | 'risky'>=35 | 'danger'<35; whitelist/stablecoin overrides exist), verdict (human string e.g. 'No issues found','Caution — has critical flags','Danger — likely rug / unsafe'), summary (string), layers (object keyed by layer name; each is a report {ok, checks[], errors[]} — failed/missing layers become {ok:false, checks:[], errors:[...], _stub:true}; each check is {id, ok, severity('critical'|'high'|'medium'|'warning'|'low'|'info'), label, detail}), providers (multiple external safety providers; each {ok, available, scoreSafe0to100, checks[], errors[]}; unkeyed providers are {ok:false, available:false, scoreSafe0to100:null, _notConfigured:true}), flags[] (every failing check across all layers+providers, sorted critical->info then by source), crossValidation {an external safety providerSafe, agree, note}, weights (scoring internals: startScore, severityPenalty, additiveScore, failingCounts, totalFailing, hardCapped, hardCapReasons, honeypotConfirmed, entityAdjusted, mature, whitelisted, regulatedStablecoin,...), entity (compact bubblemap summary {decentralizationScore, largestClusterPct, entityCount, clusteredPct, hiddenSupplyPct, entity_nakamoto, entity_gini, nodeCount, linkCount} or null), generatedAt(ms). > Cache key deepscan:, TTL 120s. Heaviest endpoint — the bundle funding-graph trace dominates cost (the Stryke frontend lazy-loads it on scroll-into-view). Engine is fail-soft: every layer/provider runs under Promise.allSettled and degrades to an error stub rather than failing the scan; a complete verdict is produced even with all third-party providers disabled. The full node/link graph is NOT here — use /{mint}/bubblemap. FAIL-SOFT: individual safety layers degrade independently to a stub rather than erroring, so a success:true verdict can be built on partial inputs; whitelist/stablecoin overrides can floor or force the grade for known blue-chips. ### GET /api/v2/tokens/{mint}/bubblemap **Bubble Map entity graph** — Returns the unified holder/funder entity graph (weighted union-find entity resolution) — nodes, links (typed + weighted with evidence), clusters, plus decentralization/concentration summary. Replaces the holder-graph provider + InsightX Atlas + an external safety provider insider graph. Cached 120s. `GET https://api.stryke.gg/api/v2/tokens/{mint}/bubblemap` · auth: `X-API-Key` **Path parameters:** - `mint` (string, optional) — SPL token mint address (base58). 400 'invalid mint' if invalid. Canonicalized internally. **Response:** Top-level (spread from analyzeBubblemap report): ok (bool), mint (canonical), nodes[], links[], clusters[], decentralizationScore (0-100 or null), largestClusterPct (number), summary{...}, checks[], errors[]. node: {id, kind, label, pctSupply, balance, rank, flags[], entity_id, degree, is_cex, is_dex, is_lp, is_supernode, fresh_wallet, first_buy_slot, age_sec} sorted by pctSupply desc then degree. link: {source, target, type (edge type e.g. same_funder/same_slot/transfer/peel), weight (number or null from EDGE_WEIGHTS), evidence (object — the WHY)}. cluster: {entity_id, kind, memberCount, pctSupply, rootFunder, collector, devLinked, confidence, kinds[], evidence[], members[]}. summary carries both raw and entity-collapsed concentration metrics (gini/hhi/nakamoto + entity_gini/entity_hhi/entity_nakamoto), clusteredPct, hiddenSupplyPct, decimals (client scales raw balances), holdersTotal (distinct owners found -> 'top N of M'). On invalid/unbuildable graph the report returns ok:false with empty nodes/links/clusters and an info check. analyzeBubblemap never throws. > Cache key bubblemap:route:, TTL 120s (the inner build memoizes bubblemap: separately). Links are capped at MAX_LINKS. Reuses memoized launch:/bundle: traces from the deepscan layers when warm so it pays no extra fetch for same-slot/funding edges — the only live work is the bounded transfer-edge scan (skippable via env). never errors on an unbuildable graph — returns ok:false with empty nodes/links (HTTP 200); links are capped and nodes are sorted by supply share. ### GET /api/v2/tokens/{mint}/safety **Safety chip (canonical, list-cheap)** — Compact per-token safety summary (0-100 HIGHER=SAFER) cheap enough to fan out across a whole list (Radar/Oracle/cards). Derives from a warm deepscan (status complete) else a cheap intel chip (status partial); never runs a fresh full deepscan. Cached 60s. A batch form GET /api/v2/tokens/safety?mints=a,b,c (<=30) returns {success,results:{:chip}}. `GET https://api.stryke.gg/api/v2/tokens/{mint}/safety` · auth: `X-API-Key` **Path parameters:** - `mint` (string, optional) — SPL mint (base58). 400 'invalid mint' if invalid. **Query parameters:** - `flags` (integer, optional, default `3`) — Max topGreen and topRed each (clamped 1-5). - `refresh` (boolean, optional, default `false`) — Bust ONLY the 60s safety memo (never triggers a full deepscan). **Response:** score 0-100 HIGHER=SAFER (== deepscan.score when warm). grade from gradeFor() (>=85 safe / >=60 caution / >=35 risky / else danger). topGreen/topRed entries {id,label,detail,severity(critical|high|medium|low|info)} are OWN-LAYER checks ONLY — vendor-named provider echoes are excluded, so NO data-vendor name appears in the body. venues deduped by dexId, primary=highest-h24-vol. source: deepscan (warm full scan, status complete) | intel (cheap chip, partial) | venues (status pending). NEVER 5xx — a failing mint degrades to status pending. > Cache key safety:, TTL 60s. Reads the warm deepscan:/intel: caches (never a fresh deepscan). Venue list via the DEX index (not the v2 key, so it does not touch the per-key breaker). Batch: GET /api/v2/tokens/safety?mints=a,b,c (<=30, per-mint degrade). ### GET /api/v2/tokens/search **Token search (superset)** — Autocomplete token search by name/symbol/address. SUPERSET of the old search entry — each result now carries full card data: image/header/description, decimals, socials_list, websites_list, multi-window priceChange/volume/txns, buy/sell counts, marketCap/fdv/liquidityUsd, pairAddress/dexId, boosts, age, base/quote tokens and DEX labels. Replaces any prior /tokens/search entry. `GET https://api.stryke.gg/api/v2/tokens/search` · auth: `X-API-Key` **Query parameters:** - `q` (string, required) — Search query (name, symbol, or mint). Minimum 2 chars; shorter returns an empty results array. **Response:** results[] — up to 20 mints, deduped by mint keeping the highest-h24-volume pair. Base fields (mint, symbol, name, image, url, priceUsd, priceChange, volume24h, liquidityUsd, marketCap, fdv, pair{}, socials{}, websites[]) come from the the DEX index shaper; the superset fields (header, description, decimals, socials_list, websites_list, volume{m5,h1,h6,h24}, txns{...}{buys,sells}, buys24h, sells24h, pairCreatedAt, ageHours, boosts, dexId, pairAddress, baseToken, quoteToken, labels) are layered on top. USD/price fields are glitch-clamped (price ≤ 1e7, USD ≤ 1e12). decimals is backfilled from the local indexer then a single batched the metadata service call. Cached 60s. > q under 2 chars returns {success:true,results:[]} (HTTP 200), never an error. ### GET /api/v2/tokens/trending/raw **Trending tokens (raw, tagged)** — Merged, deduped and enriched trending feed built from the DEX index boosts/top + boosts/latest + profiles/latest, resolved to full pairs. Each token carries source tags, dex_rank, raw (un-neutralized) image, rich price/volume/txn windows and boost amount — so a consumer can build gainers/losers/new/trending/promoted lists itself. `GET https://api.stryke.gg/api/v2/tokens/trending/raw` · auth: `X-API-Key` **Response:** tokens[] — same superset shape as /tokens/search results PLUS: image (raw the DEX index/boost icon, NOT neutralized), boosts (max boost amount seen), dex_rank (rank in the boosts/top list, null if only seen in latest/profiles feeds), tags[] (subset of 'boosted','recent_boost','new'), source ('aggregate'). Up to 90 seed mints resolved; tokens without a resolvable pair are dropped. Sorted by boosts desc then h24 volume desc. generatedAt is epoch ms; maxAgeMs is the 60s cache window. USD/price glitch-clamped. ### POST /api/v2/tokens/pairs-batch **Pairs batch (full the DEX index)** — Returns the full, glitch-sanitized primary the DEX index pair object for up to 50 mints in one call — every price/volume/txn window plus liquidity, marketCap/fdv and info{imageUrl,header,description,socials,websites}. For consumers that build their own cards. Per-mint 30s cache. `POST https://api.stryke.gg/api/v2/tokens/pairs-batch` · auth: `X-API-Key` **Body parameters:** - `mints` (string[], required) — Array of SPL mint addresses. Invalid entries are dropped; capped at the first 50 valid mints. **Response:** pairs is a map keyed by mint → full pair object (or null if no pair found for that mint). count is the number of non-null pairs. When multiple pairs exist for a mint the highest-h24-volume one wins. All USD/price fields glitch-clamped (price ≤ 1e7, USD ≤ 1e12). labels capped to 8. Each mint cached 30s independently, so overlapping batches share hits. ### GET /api/v2/tokens/{mint}/metadata-full **Token metadata (enriched)** — Off-chain enriched metadata for a Solana token: categories/tags, VC investors, supply allocation %, global rank, and a richer socials block. Curated off-chain (the market-data layer Metacore) — best for established tokens; sparse for fresh mints. `GET https://api.stryke.gg/api/v2/tokens/{mint}/metadata-full` · auth: `X-API-Key` **Path parameters:** - `mint` (string, optional) — Solana token mint (base58). Validated; invalid returns 400. **Response:** Response is normalised (numbers coerced, vendor bloat stripped) and Solana-focused; includes a derived summary where applicable. A "stale":true flag appears if served from the resilience cache during an upstream hiccup. ### GET /api/v2/tokens/screener **Token screener / search** — Search Solana tokens sortable by trendingScore24h, volume24h, liquidity, holdersCount, createdAt, or organicVolume1h (bot-filtered volume). A screener view over the token universe. `GET https://api.stryke.gg/api/v2/tokens/screener` · auth: `X-API-Key` **Query parameters:** - `input` (string, required) — Search term (symbol / name fragment). - `sortBy` (string, optional, default `volume24h`) — Sort field: trendingScore24h, volume24h, liquidity, holdersCount, createdAt, organicVolume1h. - `limit` (integer, optional, default `10`) — Results to return. Max 20. **Response:** Response is normalised (numbers coerced, vendor bloat stripped) and Solana-focused; includes a derived summary where applicable. A "stale":true flag appears if served from the resilience cache during an upstream hiccup. ### GET /api/v2/tokens/{mint}/dev-history **Dev / deployer history** — The creator/deployer wallet behind a mint plus its track record: prior launches, rug-rate, serial-/fast-rugger verdicts, dev-sold class, and funding-origin forensics (terminal funder / CEX, privacy-peel hops, operator ring). Thin wrapper over the dev-forensics engine that is also folded into /deepscan; self-resolves the deployer, so it is standalone-safe. Route memoized 2 min. `GET https://api.stryke.gg/api/v2/tokens/{mint}/dev-history` · auth: `X-API-Key` **Path parameters:** - `mint` (string, optional) — SPL mint (base58). 400 "invalid mint" if invalid. **Response:** dev is the resolved deployer; devSource is how it was resolved (e.g. "bonding_curve"). priorTokens/ruggedPriorTokens/rugRate summarize the deployer's earlier launches; serialRugger + fastRugger + medianTimeToRugSecs are behavioural verdicts (null when there is no prior history). devSoldPct/devSoldClass describe how much of THIS token the dev has offloaded. Funding origin: devFunderTerminal {address,name,category} is the terminal source (category e.g. "cex"); devFunderPeeled/privacyHops/devFunderPeel flag peel-chain obfuscation; operatorRing/operatorRingDetected/ringCoLaunchBurst flag a coordinated multi-wallet launch ring. checks[] are own-layer signals {id, ok (true=good), severity(critical|high|medium|low|info), label, detail}. errors[] lists any sub-analysis that degraded. ok:true means the report assembled (individual fields may still be null on a thin on-chain history). > Cache key devhistory:route:, TTL 120s (the engine does not self-cache the full report). Same engine surfaced inside the /deepscan verdict; call this endpoint when you want ONLY the deployer dossier without a full scan. Heavy on-chain (multi-page enhanced-tx + prior-launch liveness) — expect ~seconds on a cold mint. ### GET /api/v2/tokens/{mint}/snipers **Launch sniper cohort** — The launch-sniper cohort for a mint: how many wallets sniped the launch, what share of supply they took, slot-0 vs cohort counts, their exit distribution (still holding / sold partial / sold full / bought more), bundle concentration, and a sampled per-sniper farming profile (serial-sniper / heavy-farmer flags). Combines the launch-tape analysis with sniper forensics (which reads the same tape). Route memoized 2 min. `GET https://api.stryke.gg/api/v2/tokens/{mint}/snipers` · auth: `X-API-Key` **Path parameters:** - `mint` (string, optional) — SPL mint (base58). 400 "invalid mint" if invalid. **Response:** launchSlot/launchTime anchor the launch. sniperCount/sniperPct = wallets that bought in the sniper window and the % of supply they took; slot0SniperCount/slot0SniperPct = the subset that landed in the launch slot; cohortSniperCount = the cohort size scored by forensics. sniperExit {classified, holding, soldPartial, soldFull, boughtMore} partitions the cohort by current disposition. bundledPct/bundleWalletCount measure how much of the cohort landed in bundled (same-block/coordinated) transactions. sampledSnipers[] is a SAMPLE (not the full cohort): {address, fungibles (# of other fungible tokens the wallet holds — a farming signal), serial (repeat sniper), heavy (heavy farmer)}. serialSniperRatio is the sampled share flagged as serial snipers. Forensics runs after the launch pass (it consumes the tape the launch pass populates); if forensics degrades, sampledSnipers is [] and serialSniperRatio is null while the launch aggregates still return. > Cache key snipers:route:, TTL 120s. Same launch+sniper engine folded into /deepscan; use this for ONLY the sniper cohort. Heavy on-chain (launch tape + per-sniper sampling) — seconds on a cold mint. ### GET /api/v2/tokens/safety **Safety chips (batch)** — Batch form of the per-token safety chip (0-100 HIGHER=SAFER): fan a comma-separated mint list (max 30) into one call and get a map of chips keyed by mint. Each chip derives from a warm deepscan (status complete) else a cheap intel chip (status partial); it NEVER runs a fresh full deepscan. Per-mint degrade — a failing mint returns a pending/error chip instead of failing the batch. Sibling of GET /api/v2/tokens/{mint}/safety (single). `GET https://api.stryke.gg/api/v2/tokens/safety` · auth: `X-API-Key` **Query parameters:** - `mints` (string, required) — Comma-separated SPL mints (base58). Invalid mints are dropped; the list is capped to the first 30 valid mints. 400 "mints required …" if none are valid. - `flags` (integer, optional, default `3`) — Max topGreen and topRed each per chip (clamped 1-5). **Response:** results is a map. Each chip matches the single-mint /safety body minus the top-level success: {mint, score (0-100 HIGHER=SAFER), grade (safe|caution|risky|danger), verdict, topGreen/topRed ({id,label,detail,severity(critical|high|medium|low|info)} — OWN-LAYER checks only, no vendor names), venues (deduped by dexId, primary=highest 24h vol), liquidityUsd, source (deepscan|intel|venues|error), status (complete|partial|pending)}. topGreen/topRed are each sliced to `flags`. A mint that throws degrades to {status:"pending", score:null, grade:null, …, source:"error"} — never a 5xx. > Each chip reads the warm deepscan:/intel: caches (60s safety memo per mint); this endpoint NEVER triggers a fresh full deepscan, so it is cheap enough to fan across a whole list (Radar/Oracle/cards). Static 1-segment path — does not collide with the 2-segment /{mint}/safety. NOTE: currently this batch form is also described inline in the notes of GET /api/v2/tokens/{mint}/safety; this is its standalone entry. ## Wallets Wallet data endpoints — the core of the v2 API for portfolio dashboards, tax tools, and wallet explorers. Mounted at /api/v2/wallets (router built in the API layer, mounted via app.use('/api/v2',...) in the API layer). All endpoints require a valid X-API-Key header (validateApiKey middleware) and count against a per-key rate limit (apiLimiter); only the sibling /api/v2/health route is unauthenticated. Telemetry middleware (Stryke's engine) logs every request — including failed-auth attempts — before auth runs. Address params are validated against a base58 regex /^[1-9A-HJ-NP-Za-km-z]{32,44}$/; invalid addresses return 400 {success:false,error:'invalid wallet'}. Provider-not-configured states return 503. Upstream provider failures are caught and returned as 502 {success:false,error:}. Data sources: the market-data layer (portfolio, history, trades, activity) and Stryke's node layer (enhanced/parsed transactions, DAS metadata, RPC). The heavy /profile, /wrapped, and /roast endpoints share a 5-minute server-side cache keyed by depth+wallet(+since), set public Cache-Control with stale-while-revalidate, and build on profile.the assembler (≤5 Stryke's node layer enhanced-tx pages + 1 the market-data layer portfolio on a cold cache). ### GET /api/v2/wallets/{wallet}/portfolio **Wallet portfolio (token holdings)** — Returns the wallet's live token holdings with USD values, prices, 24h change and allocation. Primary source is the market-data layer's portfolio endpoint; falls back to the metadata service getAssetsByOwner (including native SOL) when the market-data layer isn't configured. `GET https://api.stryke.gg/api/v2/wallets/{wallet}/portfolio` · auth: `X-API-Key` **Path parameters:** - `wallet` (string, optional) — Solana wallet address (base58, 32-44 chars). Validated by SOL_ADDR_RE; invalid returns 400. **Query parameters:** - `fresh` (boolean, optional, default `false`) — Pass fresh=true to bypass the 30s server-side cache and force a live provider fetch. Any other value (or absent) serves from cache (cache.memo key 'port:', 30000ms TTL). **Response:** Top-level keys: success, wallet, then the spread of the shaped portfolio object: totalValueUSD (number), tokenCount (number), tokens (array). Each token has mint, symbol, name, image, balance, usdValue, price, priceChange24h, allocation. On the Stryke's node layer fallback path, priceChange24h and allocation are 0 and totalValueUSD is summed from per-token usdValue. SOL is injected as the first token on the fallback path when nativeBalance.lamports > 0. > Cached 30s by default via cache.memo('port:'). shapePortfolio maps the market-data layer data.assets[] (asset.contracts solana address, symbol, name, logo, token_balance, estimated_balance, price, price_change_24h, allocation) and uses data.total_wallet_balance / data.balances_length. Stryke's node layer fallback only includes FungibleToken/FungibleAsset interfaces with balance > 0. on the fallback pricing path, priceChange24h and allocation are 0, and native SOL placement differs from the primary path — do not rely on token ordering. ### GET /api/v2/wallets/{wallet}/nfts **Wallet NFTs** — Non-fungible holdings for a wallet — standard, compressed, and MPL-Core assets — resolved via the metadata/asset index. Fungible token balances are filtered out (use /portfolio for those). Paginated. Cached 60s. `GET https://api.stryke.gg/api/v2/wallets/{wallet}/nfts` · auth: `X-API-Key` **Path parameters:** - `wallet` (string, optional) — Owner wallet address (base58). 400 'invalid wallet address' if invalid. **Query parameters:** - `page` (integer, optional, default `1`) — DAS page number (1-based). - `limit` (integer, optional, default `50`) — Assets per page. Clamped to 1–100. **Response:** nfts[] each: { mint, name, symbol, image (resolved metadata image / CDN URI), collection (the collection grouping address, or null if ungrouped), compressed (true for compressed NFTs), interface (asset standard — V1_NFT, ProgrammableNFT, MplCoreAsset, etc.) }. count is the NFTs on this page after filtering out fungibles; total is the provider's total asset count for the page query (fungibles included). Airdrop / spam NFTs are not filtered — they are real on-chain holdings. > Cache key nfts:::, TTL 60s. Filters out fungible interfaces and any asset whose token decimals are greater than 0. `total` is the provider's page total INCLUDING fungibles that were filtered out, so total >= count and is NOT the NFT count; total can be null. ### GET /api/v2/wallets/{wallet}/history **Portfolio balance history** — Returns a time series of the wallet's total balance (USD value) over time from the market-data layer's wallet/history endpoint, normalized to {t,v} points. `GET https://api.stryke.gg/api/v2/wallets/{wallet}/history` · auth: `X-API-Key` **Path parameters:** - `wallet` (string, optional) — Solana wallet address (base58). Validated; invalid returns 400. **Query parameters:** - `period` (string, optional, default `30d`) — One of 1h, 1d, 7d, 30d, 90d, 365d. Any other value falls back to 30d. - `from` (number (unix), optional, default `null`) — Start timestamp; passed through to the market-data layer as 'from'. Parsed via Number(). - `to` (number (unix), optional, default `null`) — End timestamp; passed through to the market-data layer as 'to'. Parsed via Number(). - `asset` (string, optional, default `null`) — Restrict the history to a single asset/token; passed through to the market-data layer as 'asset'. **Response:** Top-level keys: success, wallet, period, from, to, series. series is an array of {t, v} points mapped from the market-data layer data.balance_history (supports both [timestamp,value] tuple and {timestamp/t, value/v} object forms); points with falsy t or non-finite v are filtered out. Cached 60s via cache.memo key 'hist:::::'. > Requires the market-data layer configured or returns 503 with success:false. the market-data layer timeout for history is 25s. requires the history provider configured or returns 503 before any work; series points with a missing timestamp or non-finite value are silently dropped (gaps are not surfaced). ### GET /api/v2/wallets/{wallet}/trades **Wallet trades (swap history)** — Returns the wallet's the market-data layer-derived swap/trade history, passed through largely verbatim from the market-data layer's wallet/trades endpoint. `GET https://api.stryke.gg/api/v2/wallets/{wallet}/trades` · auth: `X-API-Key` **Path parameters:** - `wallet` (string, optional) — Solana wallet address (base58). Validated; invalid returns 400. **Query parameters:** - `limit` (integer, optional, default `100`) — Number of trades. Clamped to 1..500 via Math.max(1, Math.min(500, Number(limit) || 100)). - `from` (number (unix), optional, default `null`) — Start timestamp; passed to the market-data layer as 'from'. - `to` (number (unix), optional, default `null`) — End timestamp; passed to the market-data layer as 'to'. **Response:** Top-level keys: success, wallet, count, trades. count is trades.length. trades is the market-data layer's data.trades array passed through unchanged (field names are the market-data layer's own, not re-shaped by this handler). Cached 60s via cache.memo key 'trades::::'. > Per CLAUDE.md the upstream the market-data layer trades provider can flake to 503; consumers should degrade gracefully. Requires the market-data layer configured. RAW PASSTHROUGH: the trades array is returned verbatim from the upstream market-data provider (provider field names, not re-shaped); count = trades.length. ### GET /api/v2/wallets/{wallet}/activity **Wallet activity feed** — Returns recent on-chain wallet events (swaps, transfers, liquidations, etc.) from the market-data layer's v2 wallet/activity endpoint with spam filtering and unlisted assets enabled. `GET https://api.stryke.gg/api/v2/wallets/{wallet}/activity` · auth: `X-API-Key` **Path parameters:** - `wallet` (string, optional) — Solana wallet address (base58). Validated; invalid returns 400. **Query parameters:** - `limit` (integer, optional, default `50`) — Number of activity items. Clamped to 1..200 via Math.max(1, Math.min(200, Number(limit) || 50)). - `order` (string, optional, default `desc`) — Sort order. 'asc' or 'desc' — anything other than 'asc' becomes 'desc'. **Response:** Top-level keys: success, wallet, count, activity. count is the length of the market-data layer's data array. activity is the market-data layer's data array passed through unchanged (the market-data layer field names). the market-data layer call sets filterSpam=true and unlistedAssets=true. Cached 30s via cache.memo key 'act:::'. > the market-data layer activity timeout is 30s. Requires the market-data layer configured. UPSTREAM-FILTERED: spam transfers are filtered out upstream, so this is NOT a complete event feed; the activity array is returned verbatim (provider field names). ### GET /api/v2/wallets/{wallet}/transactions **Decoded transactions (Stryke's node layer enhanced)** — Returns parsed, human-readable transactions for the wallet via Stryke's node layer's Enhanced Transactions REST API — each with a type, source, description, tokenTransfers and nativeTransfers. Supports keyset pagination and type/source filters. `GET https://api.stryke.gg/api/v2/wallets/{wallet}/transactions` · auth: `X-API-Key` **Path parameters:** - `wallet` (string, optional) — Solana wallet address (base58). Validated; invalid returns 400. **Query parameters:** - `limit` (integer, optional, default `50`) — Number of transactions. Clamped to 1..100 via Math.max(1, Math.min(100, Number(limit) || 50)). - `before` (string (signature), optional) — Paginate: return txs before this signature. Only applied if it matches the signature regex /^[a-zA-Z0-9]{43,128}$/. - `until` (string (signature), optional) — Paginate: return txs until this signature. Only applied if it passes signature validation. - `type` (string, optional) — Filter by Stryke's node layer transaction type (e.g. SWAP, TRANSFER, NFT_SALE). Truncated to 40 chars and passed to Stryke's node layer. - `source` (string, optional) — Filter by Stryke's node layer source (e.g. JUPITER, RAYDIUM, MAGIC_EDEN). Truncated to 40 chars and passed to Stryke's node layer. **Response:** Top-level keys: success, wallet, count, transactions. transactions is the raw Stryke's node layer Enhanced Transactions array (this handler does NOT re-shape it). Each item carries the Stryke's node layer schema: signature, timestamp, type, source, fee, feePayer, slot, description, tokenTransfers[], nativeTransfers[], instructions[], events, transactionError. count is transactions.length. > Not cached at the route layer (live Stryke's node layer call each request). Stryke's node layer caps limit at 100 internally. Requires Stryke's node layer configured. FAILED TXS: the enhanced-transactions feed returns successfully-parsed transactions only — reverted/failed transactions are NOT included in this array. To count or inspect failed transactions, use getSignaturesForAddress (inspect the `err` field) via the RPC endpoint. ### GET /api/v2/wallets/{wallet}/funding **Funding source (genesis)** — Traces a wallet's first-ever transaction to surface who funded it and with how much SOL. Uses an archival genesis-forward (oldest-first) lookup when available, else falls back to the oldest signature in the recent window. Genesis is immutable, so the result is cached 1h. `GET https://api.stryke.gg/api/v2/wallets/{wallet}/funding` · auth: `X-API-Key` **Path parameters:** - `wallet` (string, optional) — Wallet address to trace (base58). 400 'invalid wallet address' if invalid. **Response:** funded (bool). When funded:true → funder (the account that sent the most SOL in the genesis transaction), amountSol (SOL the wallet received in that tx; falls back to the funder's net debit), signature (the genesis transaction), blockTime (unix seconds), slot. funded:false (no other fields) when the wallet has no discoverable transaction history. funder is a heuristic — the non-wallet account with the largest balance decrease — so for genesis txs with multiple senders it returns the dominant source. > Cache key funding:, TTL 1h. Primary path is a single archival oldest-first transaction lookup (limit 1); the fallback walks the newest-1000 signature window and reads the oldest transaction. `funder` is a heuristic (the largest-balance-decrease account in the genesis tx) and can misattribute multi-sender/program-mediated funding; `amountSol` may include the network fee when the wallet's net receipt isn't positive. Returns funded:false when no history is discoverable. ### POST /api/v2/wallets/decode-tx **Bulk decode transactions** — Bulk-decodes arbitrary transaction signatures into Stryke's node layer's parsed/enhanced format. Send a signatures array; up to 100 valid signatures are decoded via Stryke's node layer's /v0/transactions parser. `POST https://api.stryke.gg/api/v2/wallets/decode-tx` · auth: `X-API-Key` **Body parameters:** - `signatures` (string[], required) — Array of base58 transaction signatures. Filtered by the signature regex /^[a-zA-Z0-9]{43,128}$/ and capped at the first 100 valid entries. If zero valid signatures remain, returns 400 'signatures[] required (max 100 valid sigs)'. **Response:** Top-level keys: success, count, transactions (no wallet key — this is a signature-keyed bulk decode). transactions is the raw Stryke's node layer parseTransactions array (same enhanced schema as the /transactions endpoint). count is transactions.length. > This is the only non-:wallet, body-driven route in the file and the only POST. Not cached. Stryke's node layer parseTransactions also re-filters and slices to 100. Path is literally /api/v2/wallets/decode-tx. PARTIAL RESULTS: signatures that cannot be decoded are silently dropped — count may be less than the number submitted, and results are NOT positionally aligned to the input order. Match each result by its own `signature`, not by array index. ### GET /api/v2/wallets/{wallet}/profile **Deep wallet profile** — The heavy aggregation endpoint. Walks up to 5 Stryke's node layer enhanced-tx pages plus 1 the market-data layer portfolio and computes a structured WalletProfile: identity/classification, biography, activity rhythm, per-token cohorts with rough realised PnL, behavioral fingerprint (sniper/paperhand/diamond/gambler/night-owl/discipline), velocity, sizing, risk, DEX breakdown, counterparty graph, holdings, trader tags and dozens of derived sections. `GET https://api.stryke.gg/api/v2/wallets/{wallet}/profile` · auth: `X-API-Key` **Path parameters:** - `wallet` (string, optional) — Solana wallet address (base58). Validated; invalid returns 400. **Query parameters:** - `depth` (string, optional, default `deep`) — 'fast' walks 2 Stryke's node layer pages (~200 txs); anything else (default) is 'deep' = 5 pages (~500 txs). - `since` (number (unix seconds), optional, default `null`) — Filter txs to timestamp >= since. Snapped to the nearest day (floor(since/86400)*86400) so cache keys converge. Used by /wrapped to scope to a one-year window. Non-finite or <=0 is ignored (null). **Response:** Response is {success:true,...assemble(...)}. the assembler returns ~50 top-level keys: wallet, depth, scanned (txs analyzed after the since-filter), solPriceUsd, identity {kind,label}, biography (string), activity (with topPrograms now carrying a resolved name field), risk, fingerprint, velocity, sizing (sizingRich with largestBuySymbol/avgBuyUsd/maxBuyUsd/sizePnlCorr/sizePnlSample), failures, pnl, concentration, mevAwareness, oldestPosition, biggestLesson, bestTrade, dexBreakdown, dexPnl, compounding, selfShuffler, recoveryRate, overview, hourlyPerformance, hourlyDowMatrix, holdDistribution, holdSummary, dustAttackIndex {count,total,pct}, stakingOpportunity {idleSol,stakeableSol,yearlySol,yearlyUsd,apyPct} (null when idle <= 0.05 reserve), closeableAccounts {total,empty,nonempty,recoverableSol,recoverableUsd} (null if Stryke's node layer unavailable), monthlyPnl, sharpe, lossAversion, streaks, failureBreakdown, tokenGraveyard, panicSells, hodlBenchmark, biggestDays, traderTags (array of {key,label,tone}, capped 8), cohorts {total,stillHolding,bought,top[]} (top is up to 30 enriched per-token cohorts), trades {wins[],losses[]}, counterparties (top 10, labeled with label/kind), holdings (the market-data layer portfolio: totalUsd, tokenCount, dustBags, topBags[], worst24h) or null. The example trims long arrays for brevity. > Cached 5 min server-side via cache.memo('profile::' or '...:since=', 300000ms). Sets response header Cache-Control: public, max-age=300, s-maxage=300, stale-while-revalidate=600. Cost on cold cache: up to 5 Stryke's node layer enhanced-tx pages (2 for depth=fast) + 1 the market-data layer portfolio + 1 DAS getAssetBatch + getTokenAccountsByOwner. The same profile cache is reused by /wrapped and /roast. NULLABLE SUB-OBJECTS: stakingOpportunity, closeableAccounts, holdings (and other sub-objects) can be null when their inputs are unavailable — null-check each. `depth`: only 'fast' is special-cased; any other value (including typos) is treated as 'deep' = maximum upstream cost. ### GET /api/v2/wallets/{wallet}/wrapped **Wallet wrapped (year-in-review)** — The interpretive 'wrapped' layer built on top of profile.the assembler. Maps the deep profile onto a WrappedStats object, a WalletDiagnostics object, and a wallet classification — the gentle year-in-review (no roast verdict). Reuses the same profile cache so a /profile -> /wrapped burst hits cache. `GET https://api.stryke.gg/api/v2/wallets/{wallet}/wrapped` · auth: `X-API-Key` **Path parameters:** - `wallet` (string, optional) — Solana wallet address (base58). Validated; invalid returns 400. **Query parameters:** - `depth` (string, optional, default `deep`) — 'fast' (2 Stryke's node layer pages) or 'deep' (5 pages, default). Same semantics as /profile. - `since` (number (unix seconds), optional, default `null`) — One-year-window scoping. Snapped to nearest day (floor(since/86400)*86400). Non-finite/<=0 ignored. Used to build both the profile and wrapped cache keys. **Response:** Response is {success:true, wallet, depth, stats, diagnostics, classification}. stats is Stryke's WrappedStats (address, txCount, swapCount, transferCount[always 0 GAP], failedCount, totalFeesLamports, totalPriorityFeesLamports, totalJitoTipsLamports, totalSolMovedLamports, biggestTx/biggestLossTx/oldestTx/newestTx [null GAPs], uniquePrograms, uniqueTokens, topPrograms[], topTokens[], busiestDay, jitoTipCount, dustAttacks[0 GAP], oneLiner). diagnostics is WalletDiagnostics (bot-fee fields are 0/[] GAPs, plus closeableAccounts, recoverableLamports, solBalanceLamports, pumpFunBuys[approx from dexBreakdown], walletAgeDays, lateNightSwapPct, topProgramPct, portfolio, topBag*, worstPerformer*, etc). classification is {kind,label,blurb,confidence}. No 'roast' key (see /roast). > Cached 5 min via cache.memo('wrapped::[:since=...]'), and internally memoizes the underlying 'profile::[:since=...]' so it shares the /profile cache (no extra tx walk on a warm profile). Same Cache-Control header as /profile. Built by wrapped.fromProfile() -> {stats,diagnostics,classification} (roast omitted). Several fields are documented GAPs that degrade to 0/[]/false where Stryke profile lacks the Stryke-specific input (bot fees, duplicate buys, dust attacks, all-time totals). DEGRADED FIELDS: several stats/diagnostics fields are best-effort and fall back to 0/[]/false when the underlying inputs are unavailable — do not treat these as genuine zeros. ### GET /api/v2/wallets/{wallet}/roast **Wallet roast (wrapped + verdict)** — Everything /wrapped returns PLUS a roast verdict object (headline, burns, receipts, actions, lScore, tweetText) — the full output Stryke's /roast page assembles. Same cache/cost profile as /wrapped. `GET https://api.stryke.gg/api/v2/wallets/{wallet}/roast` · auth: `X-API-Key` **Path parameters:** - `wallet` (string, optional) — Solana wallet address (base58). Validated; invalid returns 400. **Query parameters:** - `depth` (string, optional, default `deep`) — 'fast' (2 Stryke's node layer pages) or 'deep' (5 pages, default). - `since` (number (unix seconds), optional, default `null`) — One-year-window scoping. Snapped to nearest day. Non-finite/<=0 ignored. Builds both profile and roast cache keys. **Response:** Response is {success:true, wallet, depth, stats, diagnostics, classification, roast} — identical to /wrapped plus the roast key. roast is roastFromStats() output: headline (string, the highest-scoring verdict from pickVerdict), burns (string[]), receipts (array of {label,value,hint?,scope?}), actions (array of {kind,label,href,reason,potentialSavings?} — kinds: stryke/close/solfolio/wrapped, with affiliate UTM-wrapped hrefs), lScore (integer 1..99), tweetText (string). For a zero-tx wallet roast returns a fixed pristine-wallet verdict. > Cached 5 min via cache.memo('roast::[:since=...]'), internally reusing the shared 'profile::[:since=...]' cache. Same Cache-Control header as /profile and /wrapped. Built by wrapped.fromProfile() destructured to {stats,diagnostics,classification,roast}. solPriceUsd comes from the profile; falls back to 150. Roast text/verdict selection is deterministic per wallet (djb2 hash of address seeds verdict template choice). DETERMINISTIC: the roast verdict text is seeded by the wallet address — the same wallet always returns the same roast (not randomized). Zero-activity wallets return a fixed 'pristine wallet' verdict rather than an error. ### GET /api/v2/wallets/{wallet}/transfers **Wallet transfers / fund-flow** — Raw token + native-SOL transfer edges decoded from Stryke's node layer enhanced transactions, for fund-flow tracing. Filterable by mint, counterparty, and direction. With ?synthetic=1, instead returns SYNTHESIZED swap trade rows (sanitized) in the same shape /:wallet/trades emits — for wallets a market provider misses. `GET https://api.stryke.gg/api/v2/wallets/{wallet}/transfers` · auth: `X-API-Key` **Path parameters:** - `wallet` (string, optional) — Solana wallet address (base58). **Query parameters:** - `limit` (integer, optional) — Number of enhanced txs to scan (1-100, default 100). - `before` (string, optional) — Signature to paginate before (older txs). - `mint` (string, optional) — Only return transfer edges for this mint (transfer mode only). - `counterparty` (string, optional) — Only edges where this address is the sender or receiver (transfer mode only). - `direction` (string, optional) — Filter edges by direction: 'in' or 'out' (transfer mode only). - `synthetic` (string, optional) — Set to '1' or 'true' to return synthesized swap trade rows instead of raw transfer edges. **Response:** Default (transfer) mode: transfers[] of {signature, ts (epoch sec), mint, amount (UI units; SOL lamports/1e9), from, to, direction ('in' if wallet is recipient else 'out'), kind ('sol'|'token')}. Filters mint/counterparty/direction applied post-decode. With ?synthetic=1: returns {synthetic:true, count, trades[]} where each trade = {base_token, token0_address, token1_address, amount_base, amount_quote, side ('buy'|'sell'), date, hash} (only SWAP txs with both an input and output leg; QUOTE_MINTS = SOL/USDC/USDT treated as the quote side; sanitized). Enhanced-tx fetch cached 30s. ### GET /api/v2/wallets/{wallet}/stake **Wallet native stake accounts** — Native (SOL) stake accounts where the wallet is the withdrawer authority, read via Stake program getProgramAccounts (memcmp at offset 44). Returns per-account lamports/SOL, delegation state, vote account, active stake and rent-exempt flag, plus a total. `GET https://api.stryke.gg/api/v2/wallets/{wallet}/stake` · auth: `X-API-Key` **Path parameters:** - `wallet` (string, optional) — Solana wallet address (withdrawer authority). **Response:** stakes[] of {stakeAccount, lamports, sol (lamports/1e9), state ('active'|'deactivating'|'inactive'), voteAccount (delegated validator or null), activeStakeSol (delegated stake/1e9, 0 if undelegated), rentExempt}. state is 'deactivating' when a non-a platform setting deactivationEpoch is set, 'active' when delegated, else 'inactive'. totalStakedSol sums stakes[].sol. Cached 120s. ### GET /api/v2/wallets/{wallet}/tx-stats **Wallet transaction stats** — True lifetime-ish transaction and FAILED-tx counts from raw getSignaturesForAddress (the enhanced-tx feed omits failed txs and undercounts). Bounded pagination with a `capped` flag, plus age and failure-rate derived fields. `GET https://api.stryke.gg/api/v2/wallets/{wallet}/tx-stats` · auth: `X-API-Key` **Path parameters:** - `wallet` (string, optional) — Solana wallet address (base58). **Query parameters:** - `pages` (integer, optional) — Max signature pages to scan, 1000 sigs each (1-50, default 10 = up to 10k sigs). **Response:** totalTx = signatures scanned; failedCount = those with a non-null err; failureRatePct = failed/total*100 rounded to 0.1. oldestTs/newestTs are epoch seconds across the scanned window; ageDays from oldestTs rounded to 0.1 (null if no block times). capped is true when the page cap was hit (totals are a lower bound — increase pages); pagesScanned is how many 1000-sig pages were actually read. Cached 300s. ### GET /api/v2/wallets/{wallet}/defi-positions **Wallet DeFi positions** — Cross-protocol open DeFi positions for a Solana wallet — Kamino/Drift/Orca+Raydium LP, lending, and staking — each with USD value, deposits, borrows, and rewards. Complements /portfolio (spot holdings) and /stake (native stake) so net worth reflects money in DeFi, not just tokens. `GET https://api.stryke.gg/api/v2/wallets/{wallet}/defi-positions` · auth: `X-API-Key` **Path parameters:** - `wallet` (string, optional) — Solana wallet address (base58). Validated; invalid returns 400. **Response:** Response is normalised (numbers coerced, vendor bloat stripped) and Solana-focused; includes a derived summary where applicable. A "stale":true flag appears if served from the resilience cache during an upstream hiccup. ### GET /api/v2/wallets/{wallet}/pnl **Wallet PnL ledger** — Per-token profit-and-loss / cost-basis ledger for ANY Solana wallet (not just bot users): realized & unrealized PnL, average buy/sell price, buy/sell volume + counts, and fees paid. Use it to rank whales and vet copy-trade targets on hard numbers. `GET https://api.stryke.gg/api/v2/wallets/{wallet}/pnl` · auth: `X-API-Key` **Path parameters:** - `wallet` (string, optional) — Solana wallet address (base58). Validated; invalid returns 400. **Query parameters:** - `limit` (integer, optional, default `50`) — Positions to return. Clamped 1..200. - `onlyOpen` (boolean, optional, default `false`) — When true, only currently-held positions. - `sortBy` (string, optional) — Sort field (e.g. totalPnlUSD, realizedPnlUSD). **Response:** Response is normalised (numbers coerced, vendor bloat stripped) and Solana-focused; includes a derived summary where applicable. A "stale":true flag appears if served from the resilience cache during an upstream hiccup. ### GET /api/v2/wallets/{wallet}/swaps **Wallet enriched swaps (MEV / fees)** — Per-swap fee decomposition for a Solana wallet: total / gas / platform / MEV fee split plus the routing platform (e.g. axiom, gmgn, trojan). A forensics layer over the plain activity feed — surface sandwich exposure and where a wallet routes its trades. `GET https://api.stryke.gg/api/v2/wallets/{wallet}/swaps` · auth: `X-API-Key` **Path parameters:** - `wallet` (string, optional) — Solana wallet address (base58). Validated; invalid returns 400. **Query parameters:** - `limit` (integer, optional, default `50`) — Swaps to return. Clamped 1..200. - `order` (string, optional, default `desc`) — 'asc' or 'desc'. **Response:** Response is normalised (numbers coerced, vendor bloat stripped) and Solana-focused; includes a derived summary where applicable. A "stale":true flag appears if served from the resilience cache during an upstream hiccup. ### GET /api/v2/wallets/{wallet}/sandwich-rollup **MEV sandwich loss rollup** — How much a wallet lost to MEV sandwich bots across its recent swaps. Scans the wallet's recent swaps, keeps only the provider-flagged SANDWICHED ones (cap 25), and quantifies each via the per-tx MEV forensics engine (self-memoized per signature 24h) — returning confirmed sandwich events with victim loss, attacker, and extra slippage, plus a total. Route memoized 60s. `GET https://api.stryke.gg/api/v2/wallets/{wallet}/sandwich-rollup` · auth: `X-API-Key` **Path parameters:** - `wallet` (string, optional) — Solana wallet address (base58). 400 "invalid wallet" if invalid. **Query parameters:** - `limit` (integer, optional, default `40`) — How many recent swaps to scan (clamped 1-80). Only provider-flagged sandwiches among them are quantified (max 25 signatures). **Response:** swapsScanned = swaps pulled for the wallet; providerFlagged = how many carried a SANDWICH tag; confirmed = how many the on-chain forensics engine actually confirmed as sandwiches; totalVictimLossQuote = summed victim loss across confirmed events, in the QUOTE asset (SOL or USDC of the sandwiched pair). Each events[] item: {signature, confidence, attacker, victimLossQuote, attackerProfitQuote, extraSlippagePct}. events is [] (and totals 0) when nothing is flagged/confirmed — verified live against an active trader (swapsScanned:40, providerFlagged:0, confirmed:0, events:[]); the populated event above is illustrative of the shape. > Cache key sandwichRollup::, TTL 60s. Deliberately narrow provider fan-out: only provider-tagged sandwiches are re-analyzed (max 25 sigs), and the per-tx engine self-memoizes each signature 24h, so a rollup is cheap after the first pass. ### GET /api/v2/wallets/{wallet}/informed **Informed-buyer timing analysis** — Measures whether a wallet's ENTRIES are consistently well timed: for each recent buy, compares the executed price against the peak of the following window (default 3h) and reports how often that move cleared the pump threshold (default +50%). Flags a wallet only when BOTH a minimum number of well-timed entries AND a minimum hit rate are met, so a prolific trader who caught a few pumps by volume alone does not qualify. Correlation, not proof — a skilled momentum trader scores the same as someone acting on information. Coverage fields report how much of the wallet's history could actually be priced; a low `coverage` means the verdict is inconclusive rather than clean. Cached 30 min per wallet + threshold set. `GET https://api.stryke.gg/api/v2/wallets/{wallet}/informed` · auth: `X-API-Key` **Path parameters:** - `wallet` (string, required) — Base58 wallet address. **Response:** `informed` is true only when evaluated >= minEvaluated AND wins >= minWins AND winRate >= minRate.,`coverage` = evaluated / buysConsidered. Below ~0.5 treat the result as inconclusive, not clean.,`unpriced` counts buys excluded because their forward window held too few price samples to trust.,`medianSamplesPerWindow` is a data-quality readout — single-digit values mean thin price history.,Thresholds (window length, pump %, minimum wins and hit rate) are operator-tunable and are echoed in every response, so a client can always see which criteria produced the verdict rather than assuming defaults. > Costs one trade-tape fetch plus one price series per narrow time-cluster of the wallet's buys; typically 2-4s. Intended for lazy per-wallet inspection, not bulk scanning. ### POST /api/v2/wallets/cohort **Wallet cohort analysis (submit)** — Finds on-chain links between an ARBITRARY set of up to 100 wallets — no token mint involved. Traces each wallet's funding chain, direct transfers, shared funders, shared counterparties, common collectors and portfolio overlap, then clusters them into entities. Returns 202 with a jobId; poll GET /api/v2/wallets/cohort/{jobId} for progress and the result. A 100-wallet run typically completes in 15-60s. Identical address sets submitted within 10 minutes reuse the previous result (cached:true), and a concurrent identical submission joins the in-flight job instead of duplicating cost. `POST https://api.stryke.gg/api/v2/wallets/cohort` · auth: `X-API-Key` **Body parameters:** - `addresses` (string[] | string, required) — The wallets to analyse. Either an array of base58 pubkeys, or a single string with one address per line / comma / semicolon / whitespace separated (so a pasted CSV column or spreadsheet cell works). Duplicates are removed, unreadable entries are reported back in parseInfo rather than silently dropped, and the list is capped at COHORT_MAX_WALLETS (default 100). Minimum 2 valid addresses. - `refresh` (boolean, optional) — Set true to bypass the 10-minute result reuse and force a fresh analysis of the same address set. ### GET /api/v2/wallets/cohort/{jobId} **Wallet cohort analysis (result)** — Polls a cohort analysis. While running, returns status + progress (stage, done/total, pct). When status is 'done', includes the full result: a verdict (plain-language headline + tone), a cohesion score with its three components, the node/edge graph, clusters, BRIDGES (external addresses that connect wallets you supplied but did not include), the funding timeline, and coverage. Wallets whose history could not be read are returned in `unknown` — NOT in `isolated` — so unreadable data is never presented as evidence of independence. Returns 404 if the job expired (20min TTL) or the service restarted; resubmit in that case. `GET https://api.stryke.gg/api/v2/wallets/cohort/{jobId}` · auth: `X-API-Key` **Path parameters:** - `jobId` (string, required) — The jobId returned by POST /api/v2/wallets/cohort. ## Market & Charts Macro Solana market data plus every price-history and candle (OHLCV) endpoint. Aggregate 24h DEX activity across Solana (lighthouse), the canonical SOL/USD spot price and its history, and OHLCV / chart data at the token, pool, and market level. Lighter than the per-token intel endpoints — fewer upstream round-trips, longer cache TTLs. Every route is a read-only GET under /api/v2, requires an X-API-Key header, counts against your per-key rate limit, and is served from an in-process TTL+LRU cache with in-flight de-duplication (lighthouse ~5 min, sol-price ~30 s). ### GET /api/v2/market/lighthouse **Market lighthouse (24h Solana DEX stats)** — Returns aggregate 24h trading activity across Solana DEXs — total USD volume (and its 24h change), trade/buy/sell counts, and total fees paid. Sourced from the market-data layer's market/lighthouse endpoint, reshaped into a flat object and cached 5 minutes. `GET https://api.stryke.gg/api/v2/market/lighthouse` · auth: `X-API-Key` **Response:** Top-level keys come straight from the handler (the API layer lines 19-27) which maps the market-data layer's data.total.* sub-objects: volumeUSD24h = total.volumeUSD['24h']; volumeUSD24hChange = total.volumeUSDChange['24h'] (a percent change, can be negative); trades24h/buys24h/sells24h = total.trades|buys|sells ['24h'] (raw counts); feesPaidUSD24h = total.feesPaidUSD['24h']. Every field defaults to 0 if the upstream sub-key is missing, so the shape is stable even on partial upstream data. generatedAt is Date.now() captured when the cache entry was built (not per-request — value is reused for up to 5 minutes). success:true is spread alongside the data fields. Result is memoized under cache key 'lighthouse' for 5 minutes (300000 ms). > 503 { success:false, error:'lighthouse provider not configured' } is returned immediately if the platform config is unset (marketData.configured() is false). 502 { success:false, error: } on any upstream failure — the market-data layer non-2xx (thrown as 'the market-data layer market/lighthouse returned '), a 20s AbortSignal timeout, or JSON parse error. 401 (missing/invalid X-API-Key) and 429 (per-key rate limit) are enforced by the validateApiKey/rate-limit middleware at the /api/v2 mount before the handler runs. No path params, query params, or request body. Underlying provider: marketData.getLighthouse AMBIGUOUS ZEROS: each field defaults to 0 when its upstream sub-value is missing, so an all-zeros 200 can mean 'partial/empty upstream data' rather than genuinely zero volume. ### GET /api/v2/market/sol-price **Canonical SOL/USD price** — Returns the current SOL/USD spot price with a multi-source fallback chain: 30s in-memory cache → the price store DB table (accepted up to 10 min old) → Jupiter lite price API → $120 hard fallback. Always resolves to a price. `GET https://api.stryke.gg/api/v2/market/sol-price` · auth: `X-API-Key` **Response:** Fields from the API layer lines 42-48: price (number, USD); source is one of 'db' | 'jupiter' | 'fallback' indicating which tier of the chain produced the value (DB row from the shared the price store table; live Jupiter lite-api fetch; or the $120 hard fallback constant); recordedAt is epoch-ms — for 'db' it's the row's recorded_at, for 'jupiter'/'fallback' it's Date.now() at fetch time; generatedAt is Date.now() at response build. price/source/recordedAt come from solPrice.getSolPrice (Stryke's engine), memoized in-process under key 'solPrice:v2' for 30 s. On a DB miss, a live Jupiter price is fetched and opportunistically written back to the DB (fire-and-forget) so the next caller sees it. When source='fallback', price is exactly 120. > getSolPrice is designed to never throw (every tier is wrapped and the chain ends in a $120 fallback), so in practice this endpoint returns 200 with success:true even when the DB and Jupiter are both down (source:'fallback', price:120). The 502 { success:false, error: } catch branch exists in the handler but is effectively unreachable given the provider's swallow-all behavior — list it only as a theoretical upstream-error code. 401 (missing/invalid X-API-Key) and 429 (per-key rate limit) are enforced by middleware at the /api/v2 mount. No path params, query params, or request body. Tiers: in-memory memo (30s) → the database the price store latest row if <10min old → Jupiter GET https://lite-api.jup.ag/price/v3?ids=So111...112 (6s timeout) → 120 USD. FALLBACK PRICE: if both the price store and the live quote are unavailable, the endpoint returns price=120 with source='fallback' and HTTP 200 success:true. Always branch on `source` — a 'fallback' value is a hardcoded placeholder, not a live quote. ### GET /api/v2/market/sol-price/history **SOL/USD price history** — SOL/USD price time-series for charting, over a chosen range. Returns an array of [timestamp_ms, priceUsd] points (ascending), downsampled to at most 300 points. Range-specific cache TTLs. `GET https://api.stryke.gg/api/v2/market/sol-price/history` · auth: `X-API-Key` **Query parameters:** - `range` (string, optional, default `24h`) — One of: 1h, 24h, 7d, 30d. Any other value falls back to 24h. Determines lookback + native candle period (1h→5m, 24h/7d→1h, 30d→1d) and cache TTL (1h→60s, 24h→5m, 7d→15m, 30d→30m). **Response:** points is an array of [t, p] pairs: t = epoch-ms, p = SOL/USD price (> 0), sorted ascending. Non-finite or non-positive samples are dropped. When the raw series exceeds 300 points it is evenly downsampled to 300, with the true last sample always kept as the "now" end. generatedAt = Date.now() at response build. range echoes the effective range after fallback. > Cache key sol-price:history: with the per-range TTL above; concurrent hot callers share one upstream fetch. Sibling of GET /api/v2/market/sol-price (current spot). For a live single price use the spot endpoint; use this only for the chart series. ### GET /api/v2/tokens/{mint}/chart **Token price chart (OHLCV / price history)** — Returns a time-series of {t,v} price points for a mint from the market-data layer market history, suitable for candle/line charts. Cached 30s per (mint,period,from,to). `GET https://api.stryke.gg/api/v2/tokens/{mint}/chart` · auth: `X-API-Key` **Path parameters:** - `mint` (string, optional) — SPL token mint address (base58). 400 'invalid mint' if invalid. **Query parameters:** - `period` (string, optional, default `1d`) — Time bucket. Allowed: 1h, 1d, 7d, 30d, 90d, 365d. Any other value silently falls back to 1d. - `from` (number, optional, default `null`) — Start timestamp (ms epoch, passed via Number()). Optional. - `to` (number, optional, default `null`) — End timestamp (ms epoch, passed via Number()). Optional. **Response:** Top-level: mint, period (resolved/clamped; accepts `interval` as an alias), from, to (resolved window, ms), points[], resolution ('candles'|'resampled'). Each point: t (unix-ms) and v (price). Provider-agnostic via the shared resolver (price history sourced primarily from the market-data layer, with a free pool-OHLCV fallback so pump.fun mints chart). No 503 gate — serves from the free fallback when the primary key is absent/empty. Empty series → points:[]. > Cache key chart::::, TTL 30s. period is whitelisted {1h,1d,7d,30d,90d,365d} (or `interval` alias); any other value silently falls back to '1d' (no 400). Vendor-neutral: no provider name in body or errors. ### GET /api/v2/tokens/{mint}/ohlcv **OHLCV candles** — Open/high/low/close price candles, resampled from the price series into a fixed number of equal-width time buckets over the requested period. Use this for charting libraries that expect candle arrays (the /chart endpoint returns a raw price line). Cached 30s. `GET https://api.stryke.gg/api/v2/tokens/{mint}/ohlcv` · auth: `X-API-Key` **Path parameters:** - `mint` (string, optional) — SPL token mint address (base58). 400 'invalid mint' if invalid. **Query parameters:** - `period` (string, optional, default `1d`) — Look-back window. Allowed: 1h, 1d, 7d, 30d, 90d, 365d. Any other value falls back to 1d. - `buckets` (integer, optional, default `80`) — Number of candles to resample the window into. Clamped to 10–300. **Response:** Provider-agnostic. Top-level adds resolution ('candles'|'resampled') + count + from/to. candles[] ascending by time, each { t (epoch ms), o, h, l, c (USD), v (USD volume | null) }. resolution 'candles' = REAL OHLCV+volume from the free pool-OHLCV fallback (covers pump.fun mints), passed through natively (NOT re-bucketed); 'resampled' = OHLC derived from market-data-layer price points (v:null) into `buckets` equal windows. Accepts `interval` as a period alias and from/to (ms) to override the window. No 503 gate; empty series → candles:[] (never 404). Vendor-neutral body+errors. > Cache key ohlcv:::::, TTL 30s. Window bounded by from/to (ms); the series is re-filtered to the window. buckets clamped [10,300] (resampled path only — native candles pass through). Provider order: market-data layer (primary) → free pool-OHLCV fallback → keyed last-resort. Vendor-neutral. ### GET /api/v2/market/pool-ohlcv **Per-pool OHLCV candles** — OHLCV candles for a SPECIFIC liquidity pool address (not the token aggregate). For a token live on several pools (e.g. a graduated pump.fun token on both PumpSwap and Raydium), chart the exact venue a user trades. `GET https://api.stryke.gg/api/v2/market/pool-ohlcv` · auth: `X-API-Key` **Query parameters:** - `pool` (string, required) — Pool / pair address (base58). Get one from a token markets lookup. - `period` (string, optional, default `1h`) — Candle period: 1m,5m,1h,1d,1w. - `amount` (integer, optional, default `200`) — Candles to return. Max 2000. - `from` (integer, optional) — Start time (ms epoch). - `to` (integer, optional) — End time (ms epoch). **Response:** Response is normalised (numbers coerced, vendor bloat stripped) and Solana-focused; includes a derived summary where applicable. A "stale":true flag appears if served from the resilience cache during an upstream hiccup. ### GET /api/v2/tokens/{mint}/chart-source **Chart embed source resolver** — Resolves a ready-to-embed iframe src for a mint's chart, with provider failover: the DEX index (established DEX pools) first, then the pool index (pre-migration pump.fun bonding curves). Cached 10 min. `GET https://api.stryke.gg/api/v2/tokens/{mint}/chart-source` · auth: `X-API-Key` **Path parameters:** - `mint` (string, optional) — SPL token mint address (base58). 400 'invalid mint' if invalid. **Response:** On success with a resolvable pool: success, mint, available:true, provider (which chart source answered — the value is that source's own host, which is also where src and fullUrl point), src (embeddable iframe URL), fullUrl, pairAddress (the DEX index pairAddress or the pool index pool address). When no pool is found on either provider: {success:true, mint, available:false} (no provider/src). the DEX index wins if the mint has any own Solana pair (highest h24 volume); otherwise the pool index /networks/solana/tokens/{mint}/pools first pool is used. > Cache key chartSrc:, TTL 10 min. the pool index fetch is a direct fetch() with a 6s AbortSignal timeout; failures fall through to available:false rather than erroring. returns {success:true, available:false} (HTTP 200) when no chartable pool exists — branch on `available`, not on HTTP status. ## Whales Smart-money / "whale" intelligence endpoints under /api/v2/whales. Two operations: (1) bulk-classify a list of wallets into smart-money label tags and known-entity metadata (the market-data layer-derived), and (2) discover whale wallets among a token's on-chain top holders by resolving the top-20 token accounts to owner wallets (the node RPC) and enriching them with the market-data layer labels. No curated/featured list is served — callers supply their own wallet seed lists. Both endpoints require an X-API-Key header (validateApiKey) and count against the shared v2 per-key rate limit (10 requests / 10 seconds, keyed on the X-API-Key header). The /discover/:mint result is memoized in-process for 5 minutes per mint. Both depend on upstream providers (the market-data layer for labels, Stryke's node layer for on-chain data); when a provider key is unconfigured the handler returns 503, and upstream/provider failures surface as 502. ### POST /api/v2/whales/labels **Bulk classify wallet labels** — Bulk-classifies up to 100 wallet addresses, returning the market-data layer-derived smart-money flag tags (e.g. insider, dev, sniper, smart, fresh, bundler) plus known-entity name/type/logo and platform when available. `POST https://api.stryke.gg/api/v2/whales/labels` · auth: `X-API-Key` **Body parameters:** - `wallets` (string[], required) — Array of Solana wallet base58 addresses to classify. Invalid addresses (must match base58 32-44 chars) are filtered out, and the list is truncated to the first 100 valid addresses. If zero valid addresses remain after filtering, the request is rejected with 400. **Response:** success: always true on 200. count: number of wallets present in the labels map (= Object.keys(labels).length). labels: object keyed by wallet address; each value has wallet (address), entity (the market-data layer walletMetadata.entityName or null), entityType (walletMetadata.entityType or null), entityLogo (walletMetadata.entityLogo or null), platform (the market-data layer platform.name or null), and flags (the market-data layer labels[] array of smart-money tag strings, defaults to []). Only wallets the market-data layer returns data for appear in the map — addresses with no the market-data layer record are omitted, so count can be less than the number of wallets submitted. > Validation: req.body.wallets must be an array; entries are filtered through isAddr (regex /^[1-9A-HJ-NP-Za-km-z]{32,44}$/) then.slice(0,100). Provider: calls marketData.getWalletLabels which POSTs { walletAddresses } to the market-data layer /1/wallet/labels (15s timeout). Note the 503 'labels provider not configured' is returned via the bad() helper which produces { success:false, error } — code 503 despite the helper's default-400 signature. Not memoized (live each call). No request body size cap beyond the 100-address slice. Counts as one v2 rate-limited request regardless of wallet count. PARTIAL RESULTS: wallets with no entity record are omitted from the labels map, so count can be less than the number of addresses submitted; the address list is silently capped at the first 100. ### GET /api/v2/whales/discover/{mint} **Discover whales in a token's top holders** — Finds whale wallets among a token's largest holders: fetches the top-20 token accounts via the node RPC, resolves them to owner wallets, dedupes, then (if the market-data layer is configured) enriches and keeps only owners that carry smart-money flags or a known entity name. Result is cached in-process for 5 minutes per mint. `GET https://api.stryke.gg/api/v2/whales/discover/{mint}` · auth: `X-API-Key` **Path parameters:** - `mint` (string, optional) — Solana token mint address (base58, 32-44 chars). Validated with isAddr; an invalid value returns 400 'invalid mint'. **Response:** success: always true on 200. mint: echoed input mint. count: number of whales in the array. whales: array of whale objects — when the market-data layer is configured, each has wallet, entity (entityName or null), entityType (or null), entityLogo (or null), platform (platform.name or null), and flags (labels[]); only owners with at least one label OR a known entityName are included. When the market-data layer is NOT configured, whales is the raw deduped owner list as [{ wallet, flags: [] }] with no entity fields. generatedAt: epoch ms when the (cached) payload was generated — present only on the normal path; absent on the early empty-result returns ({ mint, count:0, whales:[] } when there are zero top token accounts or zero resolvable owners). Payload is memoized 5 min per mint key (whales:disc:), so generatedAt reflects cache build time, not request time. > Pipeline: node.getTokenLargestAccounts(mint) → take first 20 token-account addresses → node.getMultipleAccounts(addresses, jsonParsed) → extract data.parsed.info.owner → dedupe (Set). If the market-data layer configured, marketData.getWalletLabels(uniqOwners) filters to owners with labels.length>0 OR walletMetadata.entityName. Inner Stryke's node layer calls use.catch(()=>null) so a single provider hiccup degrades to an empty/owner-only result rather than throwing; an outer try/catch maps any thrown error to 502. Cached via cache.memo with TTL 5*60_000 ms and in-flight de-duplication (concurrent callers for the same mint share one upstream fetch). No query params or body. 503 'on-chain provider not configured' returned via bad() helper with explicit 503 code. PARTIAL / SILENT DEGRADE: only the top-20 token accounts are examined (a hard ceiling). A labels-provider hiccup degrades to an owner-only list (or empty) with HTTP 200 rather than erroring, so count can be 0 on a transient upstream issue. ### GET /api/v2/whales/featured **Featured whales directory** — Curated smart-money / whale directory, live-enriched via the market-data layer labels + portfolio. Returns net worth, asset count, top holdings and entity/flag metadata per wallet, filterable by category. Default category 'trader' so institutional CEX vaults don't dominate. `GET https://api.stryke.gg/api/v2/whales/featured` · auth: `X-API-Key` **Query parameters:** - `category` (string, optional) — Filter: 'trader' (default), 'cex', 'mm', 'validator', or 'all'. **Response:** whales[] of {wallet, entity (the market-data layer entityName or null), entityType, entityLogo, entityTwitter, platform, flags[] (raw the market-data layer labels), label (curated seed tag), category ('trader'|'cex'|'mm'|'validator', re-derived from entityType/name), netWorthUSD (clamped), assetCount, topAssets[] (top 3 by value, {symbol, logo, valueUSD})}. Sorted by netWorthUSD desc. category='all' returns every seed wallet. generatedAt is epoch ms. Underlying enrichment memoized 120s (shared with /leaderboard/featured). ### GET /api/v2/leaderboard/featured **Featured leaderboard** — Curated smart-money leaderboard ranking the same the market-data layer-enriched whale set as /whales/featured by net worth, reshaped into ranking rows. pnl30d/winRate are 0 placeholders — the market-data layer does not expose per-wallet realized PnL / win-rate here. `GET https://api.stryke.gg/api/v2/leaderboard/featured` · auth: `X-API-Key` **Query parameters:** - `category` (string, optional) — Filter: 'trader' (default), 'cex', 'mm', 'validator', or 'all'. **Response:** leaders[] of {wallet, handle (entity or curated label), displayName (same), entityLogo, flags[], category, totalValue (=netWorthUSD), pnl30d (always 0 placeholder), winRate (always 0 placeholder), tradeCount (=assetCount)}. Only wallets with totalValue > 10 kept; sorted by totalValue desc. category='all' returns all. Reuses the /whales/featured 120s memo (one shared upstream fan-out). generatedAt is epoch ms. ## Trading Swap, limit-order, and DCA endpoints backed by Jupiter's lite-api (lite-api.jup.ag). Two API routers — `swap` (mounted at /api/v2/swap) for quote + build, and `orders` (mounted at /api/v2/orders) for limit orders and dollar-cost-average (DCA) positions. Every endpoint returns UNSIGNED base64 transactions where applicable; the caller signs locally with the user's wallet and broadcasts themselves — the API never sees or holds private keys. All responses are wrapped in `{ success: true,... }` on success (the Jupiter upstream payload is spread in, or nested under `quote` for the quote endpoint). Validation failures return HTTP 400 `{ success:false, error }`; any Jupiter upstream non-2xx or timeout is surfaced as HTTP 502 `{ success:false, error }`. All endpoints require the `X-API-Key` header (validateApiKey) and count against the per-key rate limit (apiLimiter: 10 requests / 10s window, keyed by API key). Address fields are validated against a base58 Solana address regex (32-44 chars). Note: the response field names below are Jupiter lite-api passthrough shapes (swap/v1, limit/v2, dca/v1) — they are not remapped by this service. ### GET /api/v2/swap/quote **Get swap quote** — Returns the best Jupiter swap route/quote for swapping `amount` base units of `inputMint` into `outputMint`. The returned quote object is required as input to POST /api/v2/swap/build. `GET https://api.stryke.gg/api/v2/swap/quote` · auth: `X-API-Key` **Query parameters:** - `inputMint` (string, required) — Mint address of the input (sell) token. Must be a valid base58 Solana address (32-44 chars). E.g. So11111111111111111111111111111111111111112 for SOL. - `outputMint` (string, required) — Mint address of the output (buy) token. Must be a valid base58 Solana address. - `amount` (integer, required) — Input amount in the token's smallest base units (e.g. lamports for SOL). Parsed with Number(); must be finite and > 0. - `slippageBps` (integer, optional, default `50`) — Allowed slippage in basis points. Coerced via Number() then clamped to [1, 10000]; falsy/NaN falls back to 50 (0.5%). - `platformFeeBps` (integer, optional) — Optional platform fee in basis points to embed in the quote. When provided, clamped to [0, 2500]; omitted from the upstream request when absent. **Response:** `quote` is the verbatim Jupiter swap/v1 quote object. Key fields: `inAmount`/`outAmount` (base-unit strings), `otherAmountThreshold` (min received after slippage), `swapMode` (ExactIn/ExactOut), `slippageBps`, `priceImpactPct`, `platformFee` (null unless platformFeeBps was passed), and `routePlan[]` describing the AMM hops. The ENTIRE `quote` object must be passed back as `quoteResponse` to /swap/build. > Validation: bad/missing inputMint or outputMint -> 400 'inputMint, outputMint required'; non-finite or <=0 amount -> 400 'amount (in base units) required'. Upstream call has an 8s timeout; any Jupiter non-2xx or timeout -> 502 with the thrown error message (e.g. 'Jupiter swap/quote returned 4xx'). Proxies https://lite-api.jup.ag/swap/v1/quote (override via JUP_SWAP_BASE). ### POST /api/v2/swap/build **Build swap transaction** — Builds an UNSIGNED base64 swap transaction from a quote (from /swap/quote) and the user's public key. The caller signs and broadcasts it themselves; the API never holds keys. `POST https://api.stryke.gg/api/v2/swap/build` · auth: `X-API-Key` **Body parameters:** - `quoteResponse` (object, required) — The full quote object returned by GET /swap/quote (the value of its `quote` field). Must be a non-null object. - `userPublicKey` (string, required) — The wallet public key that will sign and own the swap. Must be a valid base58 Solana address. - `feeAccount` (string, optional) — Optional token account to collect the platform fee (must correspond to the platformFeeBps used in the quote). If present, must be a valid base58 address (else 400). - `computeUnitPriceMicroLamports` (integer|string, optional) — Optional priority fee (compute unit price) in micro-lamports. Forwarded to Jupiter only when truthy. **Response:** Response spreads Jupiter swap/v1 /swap output alongside `success`. Primary field: `swapTransaction` — a base64-encoded UNSIGNED versioned transaction. Also typically includes `lastValidBlockHeight`, `prioritizationFeeLamports`, `computeUnitLimit`, and (when dynamic slippage is used) `dynamicSlippageReport`. The service hardcodes wrapAndUnwrapSol:true, useSharedAccounts:false, dynamicComputeUnitLimit:true on the upstream request. > Validation: missing/non-object quoteResponse -> 400 'quoteResponse object required'; invalid userPublicKey -> 400 'userPublicKey required'; non-address feeAccount -> 400 'feeAccount must be a valid address'. Upstream timeout 12s; Jupiter non-2xx/timeout -> 502. Proxies POST https://lite-api.jup.ag/swap/v1/swap (JUP_SWAP_BASE). ### POST /api/v2/orders/limit/create **Create limit order** — Creates a Jupiter limit order and returns the UNSIGNED transaction to open it. Caller signs and broadcasts. `POST https://api.stryke.gg/api/v2/orders/limit/create` · auth: `X-API-Key` **Body parameters:** - `inputMint` (string, required) — Mint to sell. Valid base58 Solana address. - `outputMint` (string, required) — Mint to buy. Valid base58 Solana address. - `makingAmount` (string|number, required) — Amount of inputMint to sell, in base units. Validated with Number() (must be finite). - `takingAmount` (string|number, required) — Amount of outputMint to receive, in base units (defines the limit price). Validated with Number() (must be finite). - `maker` (string, optional) — Wallet that owns the order. If provided, must be a valid base58 address (else 400). Forwarded to Jupiter. - `payer` (string, optional) — Wallet that pays rent/fees for creating the order. Forwarded to Jupiter as-is (not address-validated by this service). **Response:** Spreads Jupiter limit/v2 createOrder output alongside `success`. Returns the new `order` account public key and the base64 UNSIGNED transaction (Jupiter returns this as `tx`) to open it. Exact field names are Jupiter lite-api limit/v2 passthrough. > Validation: bad inputMint/outputMint -> 400 'inputMint, outputMint required'; non-finite makingAmount/takingAmount -> 400 'makingAmount, takingAmount (base units) required'; non-address maker -> 400 'maker must be valid address'. Upstream timeout 12s. Proxies POST https://lite-api.jup.ag/limit/v2/createOrder (JUP_LIMIT_BASE). ### GET /api/v2/orders/limit **List open limit orders** — Lists a wallet's currently open Jupiter limit orders. `GET https://api.stryke.gg/api/v2/orders/limit` · auth: `X-API-Key` **Query parameters:** - `wallet` (string, required) — Wallet address whose open limit orders to fetch. Must be a valid base58 Solana address. **Response:** Spreads Jupiter limit/v2 openOrders output alongside `success` (typically an `orders` array of {publicKey, account:{maker, inputMint, outputMint, makingAmount, takingAmount,...}}). Shape is the Jupiter lite-api openOrders passthrough. > Validation: invalid/missing wallet -> 400 'wallet required'. wallet is URL-encoded into the upstream query. Default 8s timeout. Proxies GET https://lite-api.jup.ag/limit/v2/openOrders?wallet=... (JUP_LIMIT_BASE). ### GET /api/v2/orders/limit/history **Limit order history** — Returns a wallet's historical (filled/cancelled/expired) Jupiter limit orders. `GET https://api.stryke.gg/api/v2/orders/limit/history` · auth: `X-API-Key` **Query parameters:** - `wallet` (string, required) — Wallet address whose limit-order history to fetch. Must be a valid base58 Solana address. **Response:** Spreads Jupiter limit/v2 orderHistory output alongside `success` (typically an `orders` array including `status` and a `trades[]` fill log). Exact shape is the Jupiter lite-api orderHistory passthrough. > Validation: invalid/missing wallet -> 400 'wallet required'. Default 8s timeout. Proxies GET https://lite-api.jup.ag/limit/v2/orderHistory?wallet=... (JUP_LIMIT_BASE). ### POST /api/v2/orders/limit/cancel **Cancel limit order** — Builds an UNSIGNED transaction to cancel an existing Jupiter limit order. Caller signs and broadcasts. `POST https://api.stryke.gg/api/v2/orders/limit/cancel` · auth: `X-API-Key` **Body parameters:** - `order` (string|object, required) — The limit order to cancel — the order account public key (or order object) returned by /limit/create or /limit. Required; missing -> 400. Forwarded to Jupiter as the request body field `order`. - `maker` (string, optional) — The order maker / owner wallet. Forwarded to Jupiter as-is (not address-validated by this service). **Response:** Spreads Jupiter limit/v2 cancelOrder output alongside `success`. Returns the base64 UNSIGNED cancellation transaction (Jupiter `tx`). Field names are the Jupiter lite-api passthrough. > Validation: missing order -> 400 'order object required'. No address validation is performed on order/maker by this service. Default 12s timeout. Proxies POST https://lite-api.jup.ag/limit/v2/cancelOrder (JUP_LIMIT_BASE). ### POST /api/v2/orders/dca/create **Create DCA position** — Creates a Jupiter dollar-cost-average (DCA) position and returns the UNSIGNED transaction to open it. Caller signs and broadcasts. `POST https://api.stryke.gg/api/v2/orders/dca/create` · auth: `X-API-Key` **Body parameters:** - `inputMint` (string, required) — Mint to spend each cycle. Valid base58 Solana address. - `outputMint` (string, required) — Mint to accumulate. Valid base58 Solana address. - `inAmount` (string|number, required) — Total input amount to DCA across all cycles, in base units. Validated with Number() (must be finite). - `cycleSecondsApart` (string|number, required) — Seconds between each buy cycle. Validated with Number() (must be finite). - `numberOfCycles` (string|number, required) — Number of buy cycles to execute. Validated with Number() (must be finite). - `user` (string, optional) — Wallet that owns the DCA position. If provided, must be a valid base58 address (else 400). Forwarded to Jupiter. **Response:** Spreads Jupiter dca/v1 createDca output alongside `success`. Returns the new `dca` position account public key and the base64 UNSIGNED transaction (Jupiter `tx`) to open it. Field names are the Jupiter lite-api dca/v1 passthrough. > Validation: bad inputMint/outputMint -> 400 'inputMint, outputMint required'; any non-finite inAmount/cycleSecondsApart/numberOfCycles -> 400 'inAmount, cycleSecondsApart, numberOfCycles required'; non-address user -> 400 'user must be valid address'. Default 12s timeout. Proxies POST https://lite-api.jup.ag/dca/v1/createDca (JUP_DCA_BASE). ### GET /api/v2/orders/dca **List DCA positions** — Lists a wallet's Jupiter DCA positions. `GET https://api.stryke.gg/api/v2/orders/dca` · auth: `X-API-Key` **Query parameters:** - `wallet` (string, required) — Wallet address whose DCA positions to fetch. Must be a valid base58 Solana address. **Response:** Spreads Jupiter dca/v1 positions output alongside `success` (typically a positions array of {publicKey, account:{user, inputMint, outputMint, inAmountPerCycle, cycleFrequency, nextCycleAt,...}}). Shape is the Jupiter lite-api dca/v1 positions passthrough. > Validation: invalid/missing wallet -> 400 'wallet required'. Default 8s timeout. Proxies GET https://lite-api.jup.ag/dca/v1/positions?wallet=... (JUP_DCA_BASE). ### POST /api/v2/orders/dca/close **Close DCA position** — Builds an UNSIGNED transaction to close an existing Jupiter DCA position and withdraw remaining funds. Caller signs and broadcasts. `POST https://api.stryke.gg/api/v2/orders/dca/close` · auth: `X-API-Key` **Body parameters:** - `dca` (string|object, required) — The DCA position to close — the position account public key (or object) returned by /dca/create or /dca. Required; missing -> 400. Forwarded to Jupiter as the request body field `dca`. - `user` (string, optional) — The DCA position owner wallet. Forwarded to Jupiter as-is (not address-validated by this service). **Response:** Spreads Jupiter dca/v1 closeDca output alongside `success`. Returns the base64 UNSIGNED close transaction (Jupiter `tx`). Field names are the Jupiter lite-api passthrough. > Validation: missing dca -> 400 'dca object required'. No address validation on dca/user by this service. Default 12s timeout. Proxies POST https://lite-api.jup.ag/dca/v1/closeDca (JUP_DCA_BASE). ## Launches Recent Solana token launches, proxied through Stryke's on-chain indexer (HTTP GET /v1/indexer/recent-pools on Stryke-trading-api, real-time gRPC-fed). The router exposes a single read endpoint that returns the most recently indexed liquidity pools / launches, optionally filtered by DEX. Results are sorted indexed_at DESC and briefly memoized (5s) per DEX bucket. Mounted at /api/v2/launches. ### GET /api/v2/launches/recent **List recent launches** — Returns the most recently indexed token launches (newly created liquidity pools) sorted by indexed_at descending, optionally filtered to a single DEX. Backed by the trading-bot indexer's /v1/indexer/recent-pools, cached in-process for 5 seconds per DEX bucket. `GET https://api.stryke.gg/api/v2/launches/recent` · auth: `X-API-Key` **Query parameters:** - `limit` (integer, optional, default `50`) — Number of launches to return. Coerced via Number(); non-numeric / missing falls back to 50, then clamped to the inclusive range [1, 200] (Math.max(1, Math.min(200,...))). The upstream is always queried at the max (200) and the result is sliced on read, so e.g. limit=49 and limit=50 share one upstream call/cache entry. - `dex` (string, optional) — Filter to a single DEX. Lowercased before validation and must be one of an allow-list: pumpfun, pumpswap, raydium, raydium_cpmm, meteora, meteora_damm_v2, orca, bonk. Any other non-empty value returns 400. Omitted/empty means all DEXes (cache bucket 'all'). Sent upstream as dex_type. **Response:** Top-level keys: success (always true on 200), count (length of the returned launches array after limit slicing), launches (array). Each launch object is produced by normaliseLaunchRow in Stryke's engine with these fields: mint (string, token mint address); poolAddress (string, mapped from upstream pool_address); dexType (string, from dex_type — one of the allow-list values); signature (string|null, creation tx signature); blockTime (number|null, on-chain block time as unix seconds, Number()-coerced); indexedAt (number, unix timestamp when the indexer recorded the pool, Number()-coerced, the DESC sort key); creator (string|null, creator wallet); name (string|null, empty strings coalesced to null); symbol (string|null); uri (string|null, metadata URI). If the upstream returns success!==true or a non-array data, or the call times out (4s) / fails, getRecentLaunches degrades to an empty array — so a 200 with count:0 and launches:[] is the normal 'no results / soft upstream degrade' shape rather than an error. > Only router.METHOD in the API layer — the file mounts exactly this one GET handler (module.exports = router after a single router.get('/recent',...)). Implementation chain: route → cache.memo(`launches:${dex||'all'}`, 5000ms) → the indexer.getRecentLaunches(res, {limit:200, dexType:dex}) → trackedFetch GET {a server-side setting||an internal service}/v1/indexer/recent-pools?limit=200[&dex_type=...] with Bearer the platform config, 4s AbortSignal.timeout → body.data.map(normaliseLaunchRow). Caching detail: the upstream is always called at MAX_LIMIT=200 and cached per DEX bucket; the handler slices to `limit` on read (data = limit < all.length ? all.slice(0, limit) : all), so the cached payload and count reflect the requested limit, not 200. The dex value is part of the cache key, so each DEX (and 'all') has its own 5s entry. trackedFetch threads the call into per-API-key telemetry (the API layer) under provider 'indexer', endpoint 'recent-pools', contributing to provider_calls. Note the upstream itself also re-validates dex_type against its full synonym table — the local allow-list is just a fast-fail to avoid a roundtrip on clearly-invalid input. UPSTREAM DEGRADE: if the launch indexer is down or times out, the endpoint returns HTTP 200 with count:0 and launches:[]. A zero count can mean 'no recent launches' OR 'upstream unavailable' — the two are not distinguishable from the response alone. ## RPC Generic, read-only Solana JSON-RPC passthrough to Stryke's node layer mainnet, exposed under /api/v2/rpc. It replaces Stryke's previously client-exposed /api/node-rpc proxy: the same allow-list of safe, read-only RPC methods (no transaction submission, signing, or subscriptions), with per-API-key cost attribution of the upstream Stryke's node layer spend via trackedFetch. Accepts a single JSON-RPC call or a batch (max 25), forwards the body verbatim to https://mainnet.node-rpc.com, and pipes back Stryke's node layer's raw status, content-type, and body unchanged. There is exactly one route in the API layer (POST /). The unauthenticated GET /health lives in the API layer, not in this group. ### POST /api/v2/rpc **JSON-RPC passthrough (Stryke's node layer mainnet)** — Forwards a single or batched (max 25) read-only Solana JSON-RPC request to Stryke's node layer mainnet and returns the upstream response verbatim. Only allow-listed read-only methods are permitted (no transaction submission, signing, or subscriptions). `POST https://api.stryke.gg/api/v2/rpc` · auth: `X-API-Key` **Body parameters:** - `(root)` (object | object[], required) — The JSON body is EITHER a single JSON-RPC 2.0 call object OR an array of such objects (batch). An array must contain 1..25 calls (MAX_BATCH=25); empty or >25 → 400. A null/absent body → 400 'json body required'. The whole body is JSON.stringify'd and forwarded verbatim to Stryke's node layer — Stryke's node layer returns results in the same single-vs-array shape as the request. - `method` (string, required) — Per-call: the JSON-RPC method name. Must be a string (else 400 'each call needs a string method') and must be in the read-only allow-list (else 400 'method not allowed'). Allowed: getAccountInfo, getMultipleAccounts, getProgramAccounts, getBalance, getTokenAccountBalance, getTokenAccountsByOwner, getTokenAccountsByDelegate, getTokenLargestAccounts, getTokenSupply, getSlot, getBlockHeight, getBlock, getEpochInfo, getLatestBlockhash, getRecentBlockhash, getMinimumBalanceForRentExemption, getSignatureStatuses, getTransaction, getTransactionCount, getInflationReward, getStakeActivation, getVoteAccounts, getClusterNodes, getHealth, getVersion, getGenesisHash, getIdentity, getAsset, getAssetBatch, getAssetsByOwner, getAssetsByGroup, getAssetsByAuthority, getAssetsByCreator, getAssetProof, getAssetProofBatch, searchAssets, getTokenAccounts, getNftEditions, getSignaturesForAsset, getSignaturesForAddress, simulateTransaction, getPriorityFeeEstimate, getTransfersByAddress. - `jsonrpc` (string, optional) — Per-call: JSON-RPC version, e.g. "2.0". Not validated by the handler (only `method` is checked) but forwarded verbatim to Stryke's node layer, which expects "2.0". Recommended to set it. - `id` (string | number, optional) — Per-call: client-chosen request id echoed back by Stryke's node layer in the matching response object. Forwarded verbatim; not validated. - `params` (array | object, optional) — Per-call: method-specific JSON-RPC params, forwarded verbatim to Stryke's node layer (shape depends on the method — e.g. [address, {encoding}] for getAccountInfo, or {id: mint} for getAsset). Not validated by this handler. **Response:** On success the handler does NOT wrap the result — it pipes Stryke's node layer's raw body straight through with res.status(r.status).type(content-type).send(text). For a single call you get one JSON-RPC object {jsonrpc, id, result}; for a batch request you get an ARRAY of such objects. A method-level RPC failure comes back as Stryke's node layer's standard {jsonrpc, id, error:{code, message}} (often still HTTP 200, since this is an RPC-protocol error, not an HTTP error). The `result` payload shape is entirely method-dependent (e.g. getAccountInfo → {context, value}, getAsset → a DAS asset object, getSignaturesForAddress → an array of signature info). Errors RAISED BY THIS HANDLER (not Stryke's node layer) use a different shape: {success:false, error:""} — e.g. 400 {"success":false,"error":"method foo not allowed"}, 503 {"success":false,"error":"rpc provider not configured"}, 502 {"success":false,"error":""}. Note: HTTP status from Stryke's node layer is propagated as-is, so a malformed upstream request can surface a 4xx/5xx from Stryke's node layer with a node-shaped JSON-RPC error body rather than the {success:false} shape. > Single POST handler at router.post('/') in the API layer; mounted at /rpc under the /api/v2 router (the API layer: router.use('/rpc', rpc)) → full path /api/v2/rpc. Constants: MAX_BATCH=25, TIMEOUT_MS=25000 (AbortSignal.timeout). Validation order: (1) node.configured() → 503 if the platform config/the platform config unset; (2) body null → 400; (3) calls normalized to array, length 0 or >25 → 400; (4) per-call method must be a string and in ALLOW set → 400. Forwarding: trackedFetch(res,'Stryke's node layer', node.rpcUrl(), {POST, headers Content-Type+Accept application/json, body=JSON.stringify(original body), 25s timeout}, {endpoint:'jsonrpc'}); rpcUrl() = https://mainnet.node-rpc.com/?api-key=. trackedFetch attributes provider cost (Stryke's node layer = 50 micro-USD/call) and increments res.locals.providerCalls for the per-key analytics row written to the database internal storage. Auth + rate limit are applied centrally in the API layer (router.use(apiLimiter); router.use(validateApiKey)) before this sub-router, so every method here requires X-API-Key except the separate unauthenticated GET /api/v2/health. The Accept:application/json header on the outbound call is added here even though node.rpcCall() (used elsewhere) omits it — this route forwards the raw client body rather than reconstructing the JSON-RPC envelope. the request body is forwarded to the upstream RPC verbatim (only the top-level `method` is allow-listed); a method-level RPC error returns HTTP 200 with a JSON-RPC {error} body — inspect the body, not just the status. Batch is capped at 25. ## Streaming Realtime WebSocket gateway at wss://api.stryke.gg/api/v2/stream (Stryke's engine), attached to the same HTTP server as the REST API. PAID-TIER-ONLY (the stream gatekeeper in the API layer): a paid Trading-API key (rt_ on tier builder/scale/enterprise) or an operator-granted Data-API key is required; the free/self-registered tier and the public builder key are DENIED at the WS upgrade with 401. One shared reference-counted poller/feed per distinct topic fans live data to every subscriber, so backend load scales with distinct topics, not client count. Two channels: launches (new pools, optional dex filter — free) and token+mint (live per-mint trade tape — counts against the plan topic cap). Per-tier caps: builder 2 conns/2 topics, scale 20/15, enterprise 50/50, internal/data-grant 20/30 (all env-tunable). Global backstops: 500 concurrent connections, 300 distinct topics, 2048-byte messages, 30s heartbeat. All frames are JSON text: welcome / snapshot / update / pong / unsubscribed / error. ### GET /api/v2/stream **Realtime stream (WebSocket)** — WebSocket gateway at wss://api.stryke.gg/api/v2/stream (also reachable as /stream). Opened via an HTTP GET Upgrade — NOT a normal request/response. PAID-TIER-ONLY: authenticate with a paid Trading-API key (rt_… on tier builder/scale/enterprise) or an operator-granted Data-API key; the free/self-registered tier and the public builder key are DENIED at upgrade (401). Subscribe to two channels — launches (new pools, optional dex filter) and token+mint (live per-mint trade tape). One shared reference-counted poller/feed per distinct topic fans out to all subscribers, so backend load scales with distinct topics, not client count. Per-connection topic caps and per-key connection caps are enforced from the caller's tier. `GET https://api.stryke.gg/api/v2/stream` · auth: `X-API-Key` **Query parameters:** - `key` (string, optional) — API key for auth when headers are unavailable (browsers). Precedence at upgrade: Authorization: Bearer → ?key= → X-API-Key header → Sec-WebSocket-Protocol. A paid rt_ Trading-API key or an operator-granted Data-API key is required; the public builder key is always denied. **Body parameters:** - `op` (string, required) — Client→server message op (JSON text frame): "subscribe" | "unsubscribe" | "ping". ping replies {type:"pong"} (app-level keepalive; separate from the 30s WS protocol ping/pong heartbeat). - `channel` (string, required) — Required for subscribe/unsubscribe: "launches" or "token". - `dex` (string, optional) — launches channel only. Optional DEX filter; one of: pumpfun, pumpswap, raydium, raydium_cpmm, meteora, meteora_damm_v2, orca, bonk. Omit for all DEXes. Creates topic launches:. - `mint` (string, optional) — token channel only (REQUIRED there). Base58 SPL mint. Creates topic token: and counts against your plan's token-topic cap. launches subscriptions are free (do not count). **Response:** Frame types: welcome (once, on connect — advertises channels + heartbeatSec) | snapshot (once per subscribe — the current window, up to 50 items) | update (deltas only — new items since last frame) | pong (reply to op:ping) | unsubscribed (ack) | error. launches item data is the raw indexed pool row (getRecentLaunches, ≤50, sorted by time). token trade object: { mint, signature, ts (epoch-ms), type ("buy"|"sell"), priceUsd, amountToken, amountQuote (SOL or USDC depending on the pair — same field on snapshot and update), amountUsd, trader, platform }. Trades dedup by signature; last 50 retained per topic. token topics are driven by a shared live trade WS when enabled, else a 3s poll; launches always polls the indexer every 3s. Protocol heartbeat: server pings every 30s and terminates sockets that miss a pong. > PAID gate (the stream gatekeeper): (1) operator Data-API key with an explicit stream grant → allowed; the public builder key → always denied; (2) Trading-API key → allowed only on a paid tier (internal/unlimited keys map to the internal cap). Per-tier caps {connections, tokenTopics}: builder 2/2, scale 20/15, enterprise 50/50, internal/data grant 20/30 (all env-tunable). launches subscriptions are free and are NOT counted against the token-topic cap; only token: topics count. Message frames capped at 2048 bytes. Auth precedence: Authorization: Bearer → ?key= → X-API-Key → Sec-WebSocket-Protocol. Domain allow-list is checked against the browser Origin/Referer, not the API host. Copy-paste (browser/Node ws): const ws = new WebSocket("wss://api.stryke.gg/api/v2/stream?key=YOUR_PAID_KEY"); ws.onopen = () => { ws.send(JSON.stringify({op:"subscribe",channel:"launches",dex:"pumpfun"})); ws.send(JSON.stringify({op:"subscribe",channel:"token",mint:"So11111111111111111111111111111111111111112"})); }; ws.onmessage = (e) => { const f = JSON.parse(e.data); if (f.type==="update") console.log(f.channel, f.data); }; setInterval(()=>ws.readyState===1&&ws.send(JSON.stringify({op:"ping"})), 25000); ## Account & Keys Issue and manage API keys via a Solana wallet: register, regenerate, the SIWS sign-in challenge/login, and the developer dashboard (usage, fee-wallet, allowed domains, white-label license). ### POST /api/register **Register for API Key** — Creates a new API key for a Solana wallet that has no existing active key. The plaintext key is returned exactly once and cannot be retrieved later (only a hash is stored). The fee_wallet defaults to the owner wallet. Rate-limited to 5 attempts per hour per IP. `POST https://api.stryke.gg/api/register` · public **Body parameters:** - `walletAddress` (string, required) — The owner's Solana wallet address (base58). Validated by constructing a PublicKey; also used as the default fee wallet. - `email` (string, optional) — Contact email. If provided, must match a basic email regex or the request is rejected with 400. - `websiteUrl` (string, optional) — The integrator's website URL, stored on the key record. No format validation. **Response:** apiKey is the only time the plaintext key is exposed (server stores a hash via hashApiKey). dashboard is a convenience URL containing the wallet. feeStructure and nextSteps are static informational fields. > Public bootstrap endpoint (no X-API-Key). 400 if walletAddress missing, if it fails PublicKey validation ('Invalid Solana wallet address'), if email is malformed, or if the wallet already has an active key ('This wallet already has an active API key...'). 429 from registrationLimiter (max 5/hour/IP, 1-hour window). 500 on DB/insert failure. ### POST /api/auth/challenge **Request Wallet Signature Challenge** — Step 1 of wallet ownership verification. Generates a unique nonce + human-readable message that the wallet must sign off-chain. The challenge is stored server-side keyed by wallet address and expires after 5 minutes. Used as the prerequisite for /api/regenerate-key and /api/auth/wallet-login. `POST https://api.stryke.gg/api/auth/challenge` · public **Body parameters:** - `walletAddress` (string, required) — The Solana wallet address (base58) to issue a challenge for. Validated via PublicKey construction. **Response:** The client must sign the exact `message` string (UTF-8) with the wallet's private key and submit the base58 signature to /api/regenerate-key or /api/auth/wallet-login. nonce is a 32-byte hex value. expiresIn is 300 seconds. Only one pending challenge per wallet is stored (a new request overwrites the previous one). > Public bootstrap endpoint (no X-API-Key). 400 if walletAddress is missing ('Wallet address is required') or fails PublicKey validation ('Invalid Solana wallet address'). Challenges are held in an in-memory Map and swept every 60s, deleting any older than 5 minutes. No DB access, so no 500 path. ### POST /api/auth/wallet-login **Wallet Login (Session Token)** — Verifies a signature against the wallet's pending challenge and, if a matching active API key exists, issues a 1-hour dashboard session token. The session token authorizes /api/dashboard-session and /api/update-domains without exposing the API key. Returns key metadata and stats but never the plaintext API key. `POST https://api.stryke.gg/api/auth/wallet-login` · public **Body parameters:** - `walletAddress` (string, required) — The wallet address that requested the challenge. Validated via PublicKey. - `signature` (string, required) — Base58-encoded ed25519 signature of the challenge `message`, verified with tweetnacl sign.detached.verify against the wallet's public key. **Response:** sessionToken is a 32-byte hex value valid for 3600s (stored in an in-memory walletSessions Map, swept every 10 min). feeWalletBalance is in lamports via the node RPC and may be null if the balance lookup fails (non-fatal). allowedDomains is null when the key is unrestricted. On success the consumed challenge is deleted. > Public bootstrap endpoint (no X-API-Key). 400 if walletAddress/signature missing, wallet fails validation, no pending challenge exists, the challenge expired (>5 min, also deletes it), or signature decoding throws. 401 if the signature is cryptographically invalid ('Invalid signature...'). 404 if the wallet has no active key — response includes needsRegistration: true. 500 on DB failure. Note: code references walletSessions before its `const` declaration (later in the file) — relies on hoisting at call time. ### POST /api/regenerate-key **Regenerate API Key** — Step 2 (key rotation): verifies the wallet signature against its pending challenge, then issues a brand-new API key for the wallet's existing active key record, invalidating the old key. The new plaintext key is returned exactly once. Rate-limited to 5 attempts/hour/IP. `POST https://api.stryke.gg/api/regenerate-key` · public **Body parameters:** - `walletAddress` (string, required) — The owner wallet that requested the challenge. Validated via PublicKey. - `signature` (string, required) — Base58-encoded ed25519 signature of the challenge `message`, verified with tweetnacl against the wallet pubkey. **Response:** apiKey is the new plaintext key, shown only once (DB stores its hash, overwriting the prior api_key_hash on the same row id — preserving stats and settings). The previously issued key stops authenticating immediately. > Public bootstrap endpoint (no X-API-Key) but signature-gated. 400 if walletAddress/signature missing, wallet invalid, no pending challenge, challenge expired, or signature decode error. 401 if signature invalid. 404 if no active key exists for the wallet ('Please register first.'). 429 from registrationLimiter (5/hour/IP). 500 on DB failure. Consumes (deletes) the challenge on successful verification. ### GET /api/dashboard/{wallet} **Get Public Dashboard by Wallet** — Returns all API key records (active and inactive) owned by a wallet, with per-key stats and metadata. Public and unauthenticated — no signature or API key required — so it exposes only non-sensitive fields (no plaintext key, no session). Intended for a basic dashboard lookup before login. `GET https://api.stryke.gg/api/dashboard/{wallet}` · public **Path parameters:** - `wallet` (string, optional) — The owner's Solana wallet address (base58). Validated via PublicKey; invalid input returns 400. **Response:** When the wallet owns no keys, returns 200 with hasApiKey: false and a message (no apiKeys array). Returns ALL keys for the wallet ordered by created_at DESC, including inactive ones (isActive reflects per-key status). Does not include allowedDomains or any session/key secret. brandingLicenseTx is the on-chain signature once a license is active, else null. > Public, no auth despite the path comment mentioning signature. 400 if the wallet param fails PublicKey validation ('Invalid wallet address'). 500 on DB failure. The absence-of-key case is a 200 (success: true), not a 404. ### GET /api/dashboard-session **Get Dashboard via Session Token** — Returns the authenticated dashboard for the key tied to a valid wallet-login session token, including key metadata, stats, fee wallet balance, and the 20 most recent recovery transactions. Authenticated by the X-Session-Token header (issued by /api/auth/wallet-login), not by X-API-Key. `GET https://api.stryke.gg/api/dashboard-session` · auth: `X-API-Key` **Response:** recentTransactions are the latest 20 rows from sk_api_transactions for this key, newest first. feeWalletBalance is in lamports via the node RPC, null if the lookup fails. allowedDomains is null when unrestricted. This is the session-authenticated counterpart to /api/dashboard/{wallet} and returns richer (private) data. > Auth is via the X-Session-Token request header (NOT X-API-Key). 401 if the header is missing ('Session token required'), the token is unknown ('Invalid or expired session...'), or the stored session is past expiresAt (also deletes it, 'Session expired...'). 404 if the key referenced by the session no longer exists. 500 on DB failure. Sessions are in-memory (lost on server restart). ### POST /api/update-domains **Update Allowed Domains (Session)** — Sets the allowed-domain allowlist for the session's API key. Authenticated by the X-Session-Token header. Domains are normalized (trimmed, lowercased) and validated against a hostname regex (supports a leading wildcard like *.example.com); an empty/invalid list clears the restriction so the key works on any domain. `POST https://api.stryke.gg/api/update-domains` · auth: `X-API-Key` **Body parameters:** - `domains` (array, optional) — Array of domain strings to allow (e.g. ['example.com','*.app.example.com']). Each is trimmed, lowercased, and filtered by a hostname regex permitting an optional leading '*.'. If the array is empty, missing, or all entries are invalid, the domain restriction is removed (key allowed on all domains). **Response:** allowedDomains echoes the stored, normalized list, or null when restrictions were removed (in which case message is 'Domain restrictions removed'). The value is persisted as a comma-separated string in sk_api_keys.allowed_domains and enforced later by validateApiKey/isDomainAllowed. > Auth via X-Session-Token header (NOT X-API-Key). 401 if the token is missing, invalid/unknown, or expired (expired tokens are deleted). No 400 path — invalid domain entries are silently filtered out rather than rejected. 500 on DB failure. ### POST /api/dashboard/update-fee-wallet **Update Fee Wallet** — Changes the destination (fee) wallet that receives the integrator's share of recovery fees for a specific API key. Authorization is by ownership match: the UPDATE only succeeds when the supplied apiKeyId belongs to ownerWallet. No signature or session token is verified. `POST https://api.stryke.gg/api/dashboard/update-fee-wallet` · public **Body parameters:** - `ownerWallet` (string, required) — The wallet that owns the API key. Validated via PublicKey and matched against the key's owner_wallet in the WHERE clause. - `newFeeWallet` (string, required) — The new Solana wallet (base58) to receive fee payouts. Validated via PublicKey. - `apiKeyId` (number, required) — The numeric id of the API key to update. Must belong to ownerWallet or the update affects 0 rows (404). **Response:** Returns only success + message. The UPDATE is scoped by both id and owner_wallet, so a mismatch returns 404 rather than modifying another owner's key. > Despite the source comment ('requires wallet signature verification'), the handler verifies NO signature and requires no X-API-Key or session token — authorization is purely the ownerWallet/apiKeyId pair matching a DB row. 400 if any of ownerWallet/newFeeWallet/apiKeyId is missing ('Missing required fields') or if either wallet fails PublicKey validation ('Invalid wallet address'). 404 if no row matches (affectedRows === 0, 'API key not found or unauthorized'). 500 on DB failure. ### GET /api/branding-license **Get Branding License Info** — Returns static information about the white-label branding license: its cost in SOL, the payment recipient wallet, and the list of unlocked features. Public, no parameters, no DB access — purely informational for the purchase flow. `GET https://api.stryke.gg/api/branding-license` · public **Response:** cost is BRANDING_LICENSE_COST (0.5 SOL) and paymentWallet is BRANDING_LICENSE_WALLET — the values to use when constructing the payment that /api/verify-branding-payment will later validate. Fully static; identical on every call. > Public, no auth, synchronous, no DB. The handler is not async and has no try/catch, so it returns 200 in all normal cases (no documented error responses). ### POST /api/verify-branding-payment **Verify Branding License Payment** — Validates an on-chain SOL payment for the branding license and, if valid, activates the white-label license on the specified API key. Confirms the transaction on-chain, checks it is recent (<1 hour), and verifies the license wallet received at least ~0.5 SOL (1% fee tolerance). Authorization is by ownerWallet/apiKeyId ownership match. `POST https://api.stryke.gg/api/verify-branding-payment` · public **Body parameters:** - `ownerWallet` (string, required) — The wallet that owns the API key. Matched against owner_wallet for the given apiKeyId. - `transactionSignature` (string, required) — The Solana transaction signature of the 0.5 SOL payment to the branding license wallet. Fetched on-chain (commitment 'confirmed') and inspected for the payment to BRANDING_LICENSE_WALLET. - `apiKeyId` (number, required) — The numeric id of the API key to activate branding on. Must belong to ownerWallet. **Response:** On success, sets has_branding_license = TRUE and stores the signature in branding_license_tx. If the license was already active, returns 200 with alreadyActive: true and message 'Branding license already active' (no re-verification). Payment is accepted if the license wallet's balance delta >= 0.99 * 0.5 SOL (in lamports). > Public, no X-API-Key/session/signature — authorization is the ownerWallet/apiKeyId ownership match. 404 if no key matches id+owner_wallet ('API key not found or unauthorized'). 400 if any required field is missing ('Missing required fields'), the transaction is not found/unconfirmed, it is older than 1 hour ('Transaction is too old...'), or the payment amount/recipient cannot be verified ('Payment verification failed. Please send 0.5 SOL to REfUndEd1...'). 500 on DB/RPC failure. Recency check uses txInfo.blockTime. ## Recovery (v1) The v1 token-account recovery API: scan a wallet for closeable/rent-bearing SPL accounts, build a close transaction, confirm it, plus wallet analysis, token info, and aggregate stats. ### POST /api/v1/check **Check Wallets for Recoverable Rent** — Scans one or more Solana wallets for empty and balance-holding token accounts and returns the estimated SOL rent that could be recovered by closing/burning them, without producing or submitting any transactions. `POST https://api.stryke.gg/api/v1/check` · auth: `X-API-Key` **Body parameters:** - `walletAddresses` (string[], required) — Array of base58 Solana wallet addresses to scan. Must be a non-empty array with at most 20 entries. Invalid addresses are returned per-wallet with an `error` field rather than failing the whole request. - `includeAccounts` (boolean, optional) — When true, each wallet result also includes an `accounts` object with `empty` and `burnable` arrays of formatted token-account details (account, mint, displayName, owner, program, decimals, uiAmount, amountRaw, state, isFrozen, type). Defaults to false. **Response:** All SOL amounts are in SOL (lamports / LAMPORTS_PER_SOL), not lamports. `potentialRecovery`/`estimatedReturn`/`fee` are the close-only (empty-account) figures kept for backwards compatibility; `burn*` are the additional recovery from burning non-empty accounts; `total*` is close+burn combined. `canProcess` is true when the wallet has > 5000 lamports (enough for a tx fee) or has no empty accounts. Per-wallet entries that fail validation/fetch are shaped `{address, error: 'Invalid wallet address or fetch error'}` and are excluded from `totals`. `hasBrandingLicense` reflects the calling API key's branding-license flag (used by the SDK to toggle the upgrade banner). > Middleware order: apiLimiter -> validateApiKey. Read-only RPC scan via getParsedTokenAccountsByOwner for both the legacy SPL Token program and Token-2022. Recovery math uses server constants TOKEN_ACCOUNT_RENT and TOTAL_FEE_RATE; no transaction is built or submitted here (see /api/v1/close for that). ### POST /api/v1/close **Build Close/Burn Transactions** — Builds unsigned (base64-serialized) Solana transactions that close empty token accounts (and optionally burn + close balance-holding accounts) to reclaim rent, chunked to fit transaction size limits. The client signs and submits them; nothing is broadcast server-side. `POST https://api.stryke.gg/api/v1/close` · auth: `X-API-Key` **Body parameters:** - `walletAddresses` (string[], required) — Array of base58 wallet addresses (the account owners) to build transactions for. Non-empty, max 20 entries. - `refundWallet` (string, optional) — Optional base58 address that should receive the reclaimed rent (close-account destination). Defaults to each wallet's own address when omitted. - `burnAccounts` (boolean, optional) — When true, also burns tokens in non-empty accounts (then closes them) to reclaim their rent. Uses a smaller chunk size and higher compute budget per account. Defaults to false. - `selectedTokens` (string[], optional) — Optional whitelist of mint addresses to burn. Only applied when burnAccounts is true; when non-empty, only accounts whose mint is in this set are burned. Defaults to [] (burn all non-empty accounts). **Response:** `walletTransactions` is keyed by wallet address; each value is normally an array of `{transaction (base64), accountsInTx, estimatedFee (SOL), usedFeePayer}`. Wallets with no closeable/burnable accounts are skipped (omitted from the map). If a single wallet errors while building, its value becomes `{error: }` instead of an array (the overall response is still 200/success:true). Transactions are serialized with requireAllSignatures:false; when the owner balance is low (< 0.002 SOL) and a fee payer is configured, the transaction is partially signed by the platform fee payer and `usedFeePayer` is true. Fees (platform + owner split) are embedded as SystemProgram.transfer instructions; `summary` figures are in SOL. > Middleware order: apiLimiter -> validateApiKey. Returns UNSIGNED transactions only — it never broadcasts. Frozen accounts are skipped. Burn mode uses the optional 'safe burn' path: the burn wallet pre-creates its own ATAs (prepareSafeBurnAccounts) and tokens are transferred there before close; falls back to a direct on-account burn if no ATA is available. Platform-fee and owner-fee transfers are only added when the respective fee wallets are configured (and the owner fee wallet already exists on-chain with non-zero balance). After signing/submitting, clients should call /api/v1/confirm to log stats. Per-wallet build failures return {error:} for that wallet (partial success); frozen accounts are silently skipped (accountsInTx can be lower than the closeable count); platform/owner fee transfers are omitted when the respective fee wallet is absent or zero-balance. ### POST /api/v1/confirm **Confirm Transaction (Log Stats)** — Records a completed close/burn transaction for tracking and per-API-key statistics (accounts closed, SOL recovered, owner fee earned). Does not verify the transaction on-chain. `POST https://api.stryke.gg/api/v1/confirm` · auth: `X-API-Key` **Body parameters:** - `walletAddress` (string, required) — The wallet address the transaction was executed for. - `signature` (string, required) — The on-chain transaction signature to log (stored as transaction_signature). - `accountsClosed` (number, optional) — Number of token accounts closed/burned in the transaction. Defaults to 0 when omitted; added to the key's running total. - `solRecovered` (number, optional) — SOL reclaimed by the transaction. Defaults to 0; drives the platform-fee / owner-fee split and the key's running totals. **Response:** On success the handler inserts a row into sk_api_transactions and increments sk_api_keys totals (total_accounts_closed, total_sol_recovered, total_fees_earned with the owner-fee share). `stats.accountsClosed`/`solRecovered` echo the request body verbatim; `ownerFeeEarned` is computed as solRecovered * TOTAL_FEE_RATE * (1 - a platform setting). For the builder/preview key (apiKeyData.is_builder_key) the handler short-circuits: it skips all DB writes and returns success with a different message ('Builder preview transaction acknowledged (not persisted)') and stats defaulted to 0 where unset. > Middleware order: apiLimiter -> validateApiKey. Purely an accounting/telemetry endpoint — it trusts the caller-supplied accountsClosed/solRecovered and does not re-verify the signature against the chain. Builder preview key bypasses persistence to avoid foreign-key issues in the SDK demo flow. ### GET /api/v1/stats **Get API Key Stats** — Returns lifetime usage statistics for the calling API key plus its 10 most recent logged transactions. `GET https://api.stryke.gg/api/v1/stats` · auth: `X-API-Key` **Response:** `stats` is read directly off the authenticated key row (sk_api_keys); totalSolRecovered/totalFeesEarned are parseFloat'd from DECIMAL columns. `recentTransactions` is the 10 newest rows from sk_api_transactions for this key (ordered created_at DESC), mapped to {walletAddress, accountsClosed, solRecovered, feeEarned (= owner_fee), signature, createdAt}. All stats are scoped to the calling key only. > Auth via validateApiKey only — NOTE: unlike the other v1 endpoints this route is NOT wrapped by apiLimiter, so it is not subject to the apiLimiter rate limit (and thus does not return apiLimiter 429s). ### POST /api/token-info **Token Metadata (Proxy)** — Fetches token metadata for a batch of mint addresses by proxying to the public Stryke.com token-info service. Pass-through endpoint with permissive CORS. `POST https://api.stryke.gg/api/token-info` · public **Body parameters:** - `mints` (string[], required) — Array of base58 token mint addresses to look up metadata for. Must be a non-empty array; otherwise 400. **Response:** This is a thin proxy: it forwards {mints} to https://Stryke.com/api/token-info and relays the upstream response. If the upstream body is valid JSON it is returned with res.status(upstream.status).json(...); if the upstream body is not JSON, the raw text is relayed via res.send(text) with the upstream status. The exact shape is whatever the upstream service returns, so it is not fixed by this handler. > NOT protected by validateApiKey — this is a public proxy. It only sets CORS headers (Access-Control-Allow-Origin echoes the request Origin or '*'; Methods POST, OPTIONS; Headers Content-Type) and validates the mints array before forwarding. No rate limiter (apiLimiter) is attached. Defined just above the 'PUBLIC API ENDPOINTS (require API key)' section, before the v1 recovery routes. ## Direct RPC Solana RPC passthrough: send a signed transaction, check its status, and simulate before sending. ### POST /api/rpc/send **Send Transaction (RPC proxy)** — Server-side proxy that forwards a signed, base64-encoded transaction to the the node RPC `sendTransaction` method and returns the raw JSON-RPC response. Exists so the paid Stryke's node layer key never leaves the server; defaults skipPreflight=false and preflightCommitment=confirmed, which the caller can override via `options`. `POST https://api.stryke.gg/api/rpc/send` · public **Body parameters:** - `transaction` (string, required) — Base64-encoded, fully-signed serialized transaction. Required — a falsy value returns 400. - `options` (object, optional) — Optional overrides spread onto the Stryke's node layer sendTransaction config object. Defaults applied before spread: { encoding: 'base64', skipPreflight: false, preflightCommitment: 'confirmed' }. Anything here (e.g. skipPreflight, maxRetries, preflightCommitment) overrides those defaults. Defaults to {}. **Response:** The handler passes through the raw Stryke's node layer JSON-RPC envelope verbatim (`res.status(status).json(data)`), so the body is whatever Stryke's node layer returns and the HTTP status is the upstream status (typically 200). On success the envelope has `result` = the transaction signature string. If Stryke's node layer rejects the tx, the same envelope carries an `error` object ({ code, message, data }) instead of `result`, still under the upstream HTTP status. The encoding/commitment defaults are not echoed back; only the JSON-RPC envelope is returned. > PUBLIC endpoint — no X-API-Key and no rate limiter. The app.post signature is (req, res) only; the global app.use chain (line 91-92) mounts just express.json({limit:'5mb'}) and compression, and neither validateApiKey nor apiLimiter is attached. Intent (per the section banner at the API layer:2244) is to protect the paid Stryke's node layer key by keeping RPC calls server-side. Upstream call: Stryke's node layerRpcRequest('sendTransaction', [transaction, config]) at the API layer:241, which POSTs a JSON-RPC 2.0 body (id = uuidv4()) to a server-side setting. Body parsing tolerates a missing body via `req.body || {}`. Max request body 5mb (express.json limit). ABUSE SURFACE: no auth and no rate limit on this proxy to a paid upstream RPC key — any caller can submit unlimited transactions (cost-amplification / spam). A method-level RPC error returns HTTP 200 with a JSON-RPC {error} body; request body cap 5MB. ### POST /api/rpc/status **Get Signature Statuses (RPC proxy)** — Server-side proxy to the node RPC `getSignatureStatuses`. Accepts either a single `signature` or an array of `signatures`, normalizes to an array, and returns the raw JSON-RPC response. Defaults searchTransactionHistory=false (overridable via `options`). `POST https://api.stryke.gg/api/rpc/status` · public **Body parameters:** - `signatures` (string[], optional) — Array of base58 transaction signatures to look up. Takes precedence over `signature`. Either this or `signature` must be supplied and resolve to a non-empty array, or the request 400s. - `signature` (string, optional) — A single base58 transaction signature; wrapped into a one-element array if `signatures` is not provided. Either this or `signatures` is required. - `options` (object, optional) — Optional overrides spread onto the getSignatureStatuses config. Default applied before spread: { searchTransactionHistory: false }. Set { searchTransactionHistory: true } to search older confirmed transactions. Defaults to {}. **Response:** Raw Stryke's node layer JSON-RPC envelope passed through verbatim with the upstream HTTP status (typically 200). `result.value` is an array aligned 1:1 with the input signatures; each element is either a status object ({ slot, confirmations, err, confirmationStatus, status }) or null when the signature is unknown/expired from the recent cache (and searchTransactionHistory was false). On a node-level failure the envelope carries an `error` object instead of `result`. > PUBLIC endpoint — no X-API-Key, no rate limiter (app.post signature is (req, res) only). Input normalization: const sigArray = signatures || (signature ? [signature] : null); validated with `!sigArray || !Array.isArray(sigArray) || !sigArray.length`. Upstream: Stryke's node layerRpcRequest('getSignatureStatuses', [sigArray, { searchTransactionHistory: false,...options }]). Part of the RPC-proxy block (the API layer:2244) that shields the paid Stryke's node layer key. No auth / no rate limit (paid-RPC proxy); a method-level RPC error returns HTTP 200 with a JSON-RPC {error} body — inspect the body, not the status. ### POST /api/rpc/simulate **Simulate Transaction (debug)** — Debug endpoint that deserializes a base64 transaction (tries VersionedTransaction, falls back to legacy Transaction) and simulates it against the live connection with sigVerify=false and replaceRecentBlockhash=true. Returns a custom summary of the simulation rather than a raw RPC envelope. `POST https://api.stryke.gg/api/rpc/simulate` · public **Body parameters:** - `transaction` (string, required) — Base64-encoded serialized transaction (signed or unsigned — signatures are not verified during simulation). Required; falsy returns 400. Parsed as a VersionedTransaction first, then legacy Transaction.from() as fallback. - `options` (object, optional) — Optional overrides spread onto connection.simulateTransaction config. Defaults applied before spread: { sigVerify: false, replaceRecentBlockhash: true }. Can override these or add fields like { accounts: { addresses: [...] }, commitment }. Defaults to {}. **Response:** Unlike /send and /status, this returns a custom-shaped 200 body (not the raw JSON-RPC envelope) built from simulation.value: `success` (always true when simulation completes, even if the tx itself errored), `err` (the on-chain simulation error or null), `logs` (full program-log array, [] if none), `unitsConsumed` (compute units), `accountsReturned` (count of returned accounts, 0 if none), and `simulationDetails` { hasError: !!err, computeUnits, logCount }. A transaction that fails on-chain still yields HTTP 200 with success:true and a populated `err` — inspect `err`/`simulationDetails.hasError`, not the HTTP status, to know if the tx would fail. > PUBLIC endpoint — no X-API-Key, no rate limiter (app.post signature is (req, res) only). Labeled a debug endpoint in code (comment at the API layer:2290). Uses getConnection() (the API layer:471) for a live Solana RPC connection rather than the Stryke's node layerRpcRequest passthrough helper, then connection.simulateTransaction(tx, { sigVerify:false, replaceRecentBlockhash:true,...options }). NODE_ENV-gated `stack` is exposed only in the 'Simulation failed' (inner 500) branch, not the outer 500. Logs first 10 simulation logs server-side. Part of the RPC-proxy block (the API layer:2244). success:true means the simulation COMPLETED, not that the tx would land — a reverting tx still returns 200 with a populated `err`. No auth / no rate limit; the 500 paths surface a raw error string. ## System Service health and status. Unauthenticated — use it for uptime monitoring. ### GET /api/v2/health **Health check** — Liveness probe. Returns ok + the API version + a server timestamp. No API key required. (The root /health alias returns the same.) `GET https://api.stryke.gg/api/v2/health` · public **Response:** ok is always true when the service is up. ts is server epoch-ms. Use for monitors/load-balancer health. > Unauthenticated. Cheap — safe to poll frequently. ### GET /api/v2/stats **Per-key usage stats** — Usage dashboard for the calling API key over a time range: total calls, error/failure rates, latency, provider spend, a top-routes breakdown, and a daily series for charts. Scoped to the authenticated key only (reads the caller's own call log). Defaults to 7d. `GET https://api.stryke.gg/api/v2/stats` · auth: `X-API-Key` **Query parameters:** - `range` (string, optional, default `7d`) — Window: 24h, 7d, 30d, or 90d. Any other value falls back to 7d. **Response:** range_ms = the resolved window in ms; since = window start (epoch-ms). summary: calls, errors (status>=400), failures (status>=500), error_rate/failure_rate (fractions of calls), avg_ms/max_ms (latency), provider_calls (upstream calls made on your behalf), provider_cost_usd (your attributed upstream spend). byRoute[] (top 30 by calls): {route, calls, errors, avg_ms, cost_usd}. series[] (one row per UTC day, ascending): {ts (day bucket, epoch-ms), calls, failures, cost_usd}. All figures are scoped to the calling key only. > Reads the per-key API call log; requires a real registered key (req.apiKeyData.id). VERIFY NOTE: could not exercise live with the public builder preview key — it has no server-side key id, so the endpoint correctly returns 401 {success:false,error:"no key context"}; the responseExample is reconstructed from the API layer field-for-field. Mounted at /api/v2/stats (router GET /). ## Trading API: Trade & Execution Auth: send your key as `Authorization: Bearer ` (the trading API uses Bearer, NOT the X-API-Key header used by the data API). Build, sign, and broadcast swaps across all supported DEXes plus the on-chain refund/recovery flow. trade scope; per-tenant; supports an idempotency key. ### POST /v1/trade/buy **Buy token** — Buy a token with the caller's per-user wallet using the same TradeExecutor as the Telegram bot. Auto-detects DEX (or honors dex/pool_address hints), injects the app's atomic platform fee, and supports SOL or USDC/USDT settlement (pay_with_mint). Two modes: custodial (server signs+sends, returns signature) or client_signs:true (returns an unsigned base64 tx for the caller to sign+broadcast). Idempotent via the Idempotency-Key header. `POST https://api.stryke.gg/v1/trade/buy` · auth: `Authorization: Bearer` **Body parameters:** - `mint` (string, required) — Token mint to buy (base58). - `amount_in` (number, required) — Amount to SPEND — SOL by default, or whole pay_with_mint units (e.g. USDC). Must be 0.00001–100. Aliases: amount_sol, amount. - `dex` (string, optional) — DEX to trade on (auto-detected if omitted): pumpfun, pumpswap, raydium_cpmm, etc. Alias: dex_type. - `pool_address` (string, optional) — Specific pool/pair address, skips detection. Alias: pair_address. - `slippage_bps` (int, optional) — Slippage tolerance in bps (default 1000 = 10%). Max 100000. - `mev_protect` (bool, optional) — Route via Jito bundle for MEV protection (default false). - `tip_lamports` (int, optional) — MEV/priority tip in lamports (default 1500000 = 0.0015 SOL). - `compute_unit_price` (int, optional) — Priority fee, micro-lamports per CU (default V1_DEFAULT_CU_PRICE, 500000). Alias: cu_price. - `priority_micro_lamports` (int, optional) — Priority fee µL/CU; takes precedence over compute_unit_price. Clamped to V1_MAX_PRIORITY_FEE_LAMPORTS. Aliases: priorityFee, priority_fee. - `pay_with_mint` (string, optional) — Settlement/input currency mint to pay with (default SOL). Accepts SOL/WSOL/USDC/USDT only; token must be in a pool quoted in that currency. Alias: input_mint. - `client_signs` (bool, optional) — If true, return an unsigned base64 tx instead of executing (caller signs + broadcasts). Requires wallet pubkey. - `wallet` (string, optional) — Trading wallet pubkey (required for client_signs payer). Alias: trading_wallet. - `use_router` (bool, optional) — Route swap through the on-chain Stryke Router (default false = direct DEX). Only honored on client_signs builds. Aliases: useRouter, route_via_program. - `mev_tip_lamports` (int, optional) — Jito tip (total lamports) baked into a client_signs build; omit → 100000 default, 0 → opt-out. Aliases: mevTipLamports, mevTip, jitoTip. - `mev_tip_bps` (int, optional) — Jito tip as bps of quoted trade value (resolved server-side). Overridden by mev_tip_lamports. Alias: mevTipBps. - `fee_bps` (int, optional) — Per-trade platform fee bps override — only honored with the fee_routing scope (else 403). Alias: fee_bps_override. - `fee_wallet` (string, optional) — Override destination wallet for this trade's platform fee — only with fee_routing scope (else 403). Alias: feeWallet. - `nonce_account` (string, optional) — Durable nonce account pubkey (Turbo Mode); pass with nonce_value. - `nonce_value` (string, optional) — Current nonce hash (base58); required with nonce_account. - `simulate` (bool, optional) — Simulate only — runs full pipeline via simulateTransaction, no on-chain submit (default false). **Response:** Custodial mode returns the TradeResponse struct: success, signature, amount_received (BUY = tokens received, raw units), amount_spent (BUY = SOL lamports spent), timing (per-stage ms + confirm_method/mev_service/dex_name/landed_slot). simulate adds simulated:true, compute_units, logs[]. client_signs:true instead returns {"success":true,"data":{"unsigned_tx_base64":"...","recent_blockhash":"...","last_valid_block_height":123}}. > Idempotency-Key header supported (tenancy idempotency_layer). X-User-Ref header identifies the per-user wallet. ### POST /v1/trade/sell **Sell token** — Sell a token from the caller's per-user wallet via the same TradeExecutor as the bot. Sell by percent (default 100) or exact amount_in (base units, converted to a percent of the ATA balance). Injects the app's platform fee (deducted from output), supports SOL or USDC/USDT settlement (receive_mint), and offers custodial or client_signs unsigned-tx modes. Closes/records the position on a 100% sell. `POST https://api.stryke.gg/v1/trade/sell` · auth: `Authorization: Bearer` **Body parameters:** - `mint` (string, required) — Token mint to sell (base58). - `percent` (int, optional) — Percent of balance to sell, 1–100 (default 100 when amount_in omitted). - `amount_in` (int, optional) — Exact token amount (base units) to sell; converted to a percent of the ATA balance. Alias: amount_tokens. - `dex` (string, optional) — DEX to trade on (auto-detected if omitted). Alias: dex_type. - `pool_address` (string, optional) — Specific pool/pair address, skips detection. Alias: pair_address. - `slippage_bps` (int, optional) — Slippage tolerance in bps (default 1000). Max 100000. - `mev_protect` (bool, optional) — Route via Jito bundle (default false). - `close_token_account` (bool, optional) — Close the token ATA after a full sell to reclaim rent. Alias: close_ata. - `tip_lamports` (int, optional) — MEV/priority tip in lamports (default 1500000). - `compute_unit_price` (int, optional) — Priority fee µL/CU (default 500000). Alias: cu_price. - `priority_micro_lamports` (int, optional) — Priority fee µL/CU; precedence over compute_unit_price; clamped. Aliases: priorityFee, priority_fee. - `receive_mint` (string, optional) — Currency to RECEIVE (default SOL); accepts SOL/WSOL/USDC/USDT; token must be in a pool quoted in it. Alias: output_mint. - `client_signs` (bool, optional) — If true, return an unsigned base64 tx instead of executing. - `wallet` (string, optional) — Trading wallet pubkey (client_signs payer). Alias: trading_wallet. - `use_router` (bool, optional) — Route through the on-chain Stryke Router (default false = direct DEX). client_signs only. Aliases: useRouter, route_via_program. - `mev_tip_lamports` (int, optional) — Jito tip (total lamports) for a client_signs build; omit → 100000, 0 → opt-out. Aliases: mevTipLamports, mevTip, jitoTip. - `mev_tip_bps` (int, optional) — Jito tip as bps of estimated SOL proceeds (resolved server-side). Overridden by mev_tip_lamports. Alias: mevTipBps. - `fee_bps` (int, optional) — Per-trade platform fee bps override — fee_routing scope only (else 403). Alias: fee_bps_override. - `fee_wallet` (string, optional) — Override platform-fee destination — fee_routing scope only (else 403). Alias: feeWallet. - `nonce_account` (string, optional) — Durable nonce account pubkey (Turbo Mode). - `nonce_value` (string, optional) — Current nonce hash; required with nonce_account. - `simulate` (bool, optional) — Simulate only, no on-chain submit (default false). **Response:** Custodial mode returns TradeResponse: amount_received (SELL = SOL lamports received), amount_spent (SELL = token amount sold), timing. client_signs:true returns {"success":true,"data":{"unsigned_tx_base64":"...","recent_blockhash":"...","last_valid_block_height":123}}. simulate adds simulated/compute_units/logs. > Idempotency-Key header supported. On a 100% custodial sell the position is closed with PnL in the background. ### POST /v1/trade/buy-multi-hop **Buy token (multi-hop alias)** — Convenience alias that delegates to /v1/trade/buy with SOL settlement (the executor handles SOL↔USDC bridging automatically). Accepts a slim multi-hop body; bridge_slippage_bps and intermediate_mint are accepted but ignored (routing is automatic). `POST https://api.stryke.gg/v1/trade/buy-multi-hop` · auth: `Authorization: Bearer` **Body parameters:** - `mint` (string, required) — Token mint to buy. - `amount_sol` (number, required) — SOL to spend. - `dex_type` (string, optional) — Optional DEX hint. - `pair_address` (string, optional) — Optional pool/pair address hint. - `slippage_bps` (int, optional) — Slippage tolerance in bps (default 1000). - `mev_protect` (bool, optional) — Route via Jito bundle (default false). - `bridge_slippage_bps` (int, optional) — Accepted but ignored — multi-hop routing/bridging is automatic. - `intermediate_mint` (string, optional) — Accepted but ignored — bridge currency is chosen automatically. **Response:** Identical response shape to /v1/trade/buy (custodial TradeResponse). Tip/cu_price default; nonce and simulate are forced off by the alias. > Thin wrapper over v1_buy — multi-hop bridging is automatic in the executor. Idempotency-Key supported. ### POST /v1/trade/sell-multi-hop **Sell token (multi-hop alias)** — Convenience alias that delegates to /v1/trade/sell with SOL settlement (the executor handles SOL↔USDC bridging automatically). bridge_slippage_bps and intermediate_mint are accepted but ignored. `POST https://api.stryke.gg/v1/trade/sell-multi-hop` · auth: `Authorization: Bearer` **Body parameters:** - `mint` (string, required) — Token mint to sell. - `percent` (int, optional) — Percent of balance to sell, 1–100 (default 100). - `amount_tokens` (int, optional) — Exact token amount (base units) to sell. - `dex_type` (string, optional) — Optional DEX hint. - `pair_address` (string, optional) — Optional pool/pair address hint. - `slippage_bps` (int, optional) — Slippage tolerance in bps (default 1000). - `mev_protect` (bool, optional) — Route via Jito bundle (default false). - `close_ata` (bool, optional) — Close the token ATA after a full sell. - `bridge_slippage_bps` (int, optional) — Accepted but ignored — routing/bridging is automatic. - `intermediate_mint` (string, optional) — Accepted but ignored — bridge currency is chosen automatically. **Response:** Identical response shape to /v1/trade/sell (custodial TradeResponse). Tip/cu_price default; nonce and simulate forced off by the alias. > Thin wrapper over v1_sell — multi-hop bridging is automatic. Idempotency-Key supported. ### GET /v1/trade/pool-params **Pool params** — Resolve and return the on-chain pool parameters for a mint (DEX type, quote mint, SOL-quoted flag) using the executor's cached pool-param fetch. Useful for verifying routing before a trade. Read-only, no wallet needed. `GET https://api.stryke.gg/v1/trade/pool-params` · auth: `Authorization: Bearer` **Query parameters:** - `mint` (string, required) — Token mint (base58). - `dex_type` (string, optional) — Optional DEX hint to skip detection. - `pair_address` (string, optional) — Optional pool/pair address hint. **Response:** is_sol_quoted: whether the pool is quoted in SOL/WSOL. quote_mint: the pool's quote currency mint (null if none). cache_hit: pool-params cache hit. fetch_ms/detect_ms: timings. ### GET /v1/trade/estimate **Estimate output** — Estimate the output and price impact for a buy or sell of a given amount, using live (best-effort on-chain refreshed) reserves from the resolved pool. Same path as /v1/trade/quote's underlying estimate but with simple query params and no fee accounting. `GET https://api.stryke.gg/v1/trade/estimate` · auth: `Authorization: Bearer` **Query parameters:** - `mint` (string, required) — Token mint (base58). - `amount` (int, optional) — Input amount in raw base units (lamports for a SOL buy, token base units for a sell). - `side` (string, optional) — buy or sell (default buy). - `dex_type` (string, optional) — Optional DEX hint. - `pair_address` (string, optional) — Optional pool/pair address hint. **Response:** estimated_output: estimated tokens (buy) or SOL/quote lamports (sell) for the given amount. price_impact_pct: estimated price impact percent. Reflects fresh reserves written back to cache so a subsequent build matches. ### POST /v1/trade/quote **Trade quote** — Pre-trade preview WITHOUT building a transaction. One side of input_mint/output_mint must be a quote currency (SOL/WSOL/USDC/USDT), the other the token; buy = spend quote→receive token, sell = sell token→receive quote. Returns gross + net (after platform fee) outputs, the slippage floor the built tx will enforce, price impact, and the route/DEX. Rejects token↔token and cross-currency mismatches. `POST https://api.stryke.gg/v1/trade/quote` · auth: `Authorization: Bearer` **Body parameters:** - `input_mint` (string, required) — Mint being spent (base58). For a buy this is the quote currency; for a sell it is the token. - `output_mint` (string, required) — Mint being received (base58). For a buy this is the token; for a sell it is the quote currency. - `amount` (int, required) — Amount of input_mint in raw base units. - `slippage_bps` (int, optional) — Slippage tolerance in bps (default 1000), used to compute min_out_amount. - `dex_type` (string, optional) — Optional DEX hint to skip detection. - `pair_address` (string, optional) — Optional pool/pair address hint. **Response:** out_amount = gross swap output; min_out_amount = slippage floor the built tx encodes. net_out_amount/min_net_out_amount account for the platform fee (BUY net==gross tokens, fee is extra cost; SELL net = gross − fee). platform_fee is in the quote currency. price_impact_bps, route, and token block included. ### GET /v1/trade/bundle-status **Bundle / landing status** — Report the ACTUAL landing of an MEV broadcast so a client can honestly badge 'MEV protected' only when the Jito bundle itself landed. Pass bundle_id (from the broadcast response) for Jito getBundleStatuses attribution and/or signature for definitive on-chain landing (incl. the RPC-fallback leg). At least one is required. `GET https://api.stryke.gg/v1/trade/bundle-status` · auth: `Authorization: Bearer` **Query parameters:** - `bundle_id` (string, optional) — Bundle id from the broadcast response (Jito attribution). Preferred for MEV badging. - `signature` (string, optional) — Tx signature for definitive on-chain landing (catches the RPC-fallback leg). **Response:** landed: tx on-chain via either leg. mev_protected: true ONLY when the Jito bundle itself landed (not the public-RPC fallback). bundle_status ∈ landed/failed/pending/unknown; onchain_status ∈ landed/failed/pending/unavailable. Keys appear only for the inputs provided. ### GET /v1/trade/balance **Token balance** — Return the token balance for the requesting user's wallet, split across the seed-derived and standard ATAs. Resolves the per-user wallet from the authenticated session/X-User-Ref. `GET https://api.stryke.gg/v1/trade/balance` · auth: `Authorization: Bearer` **Query parameters:** - `mint` (string, required) — Token mint (base58). **Response:** total_balance = seed + standard; the two ATA balances are broken out (raw base units). ### GET /v1/trade/sol-balance **SOL balance** — Return the SOL balance (lamports + SOL) for the requesting user's wallet, resolved from the authenticated session/X-User-Ref. No params. `GET https://api.stryke.gg/v1/trade/sol-balance` · auth: `Authorization: Bearer` **Response:** balance_lamports raw; balance_sol = lamports / 1e9. ### GET /v1/trade/detect **Detect DEX** — Auto-detect the DEX/pool for a mint without executing a trade, using the same token-info path the bot uses when a user pastes an address (the DEX index + indexer). Returns DEX type, pair address, and basic name/symbol. `GET https://api.stryke.gg/v1/trade/detect` · auth: `Authorization: Bearer` **Query parameters:** - `mint` (string, required) — Token mint (base58). **Response:** data_source = where the info came from (indexer/the DEX index/etc.). detect_ms = detection latency. ### POST /v1/trade/broadcast **Broadcast signed tx** — Relay a client-signed base64 VersionedTransaction to the network through the executor's SWQOS-aware submit pipeline (same endpoints/pool as custodial trades). Auto-detects an embedded Jito tip and offers the tx via sendBundle first (with public-RPC fallback). submitted_via reflects the attempted strategy, NOT a landing guarantee — poll /v1/trade/bundle-status. `POST https://api.stryke.gg/v1/trade/broadcast` · auth: `Authorization: Bearer` **Body parameters:** - `signed_tx_base64` (string, required) — Base64-encoded signed VersionedTransaction (max 2 KiB on the wire). - `tip_lamports` (int, optional) — Informational only — tip already embedded in the signed tx; the submit path adds no tip instructions. **Response:** submitted_via: jito_bundle (tip detected) | swqos (no tip) | fallback_rpc (no executor wired). bundle_id returned for bundle attribution; rpc_fallback true when a public fallback leg exists; landing is always 'pending' at broadcast time. > Idempotency-Key supported. user_id (tenancy) validated but not otherwise used. ### GET /v1/token/info **Token info** — Full token info (price, market cap, liquidity, 24h volume, DEX/pair, bonding-curve and authority flags) using the same fetch path the bot uses when a user pastes an address. No wallet needed. `GET https://api.stryke.gg/v1/token/info` · auth: `Authorization: Bearer` **Query parameters:** - `mint` (string, required) — Token mint (base58). **Response:** Includes price_usd/price_sol, market_cap, liquidity, volume_24h, DEX info, plus is_bonding_curve/bonding_curve_percent/is_renounced/is_freezable risk flags. ### GET /v1/test/discover **Discover pools on-chain** — Diagnostic: directly run on-chain pool discovery for a mint (PDA batch + GPA scan), bypassing the indexer DB. Returns every pool found with its DEX, quote mint, and program id, plus discovery latency. Intended for routing/debugging. `GET https://api.stryke.gg/v1/test/discover` · auth: `Authorization: Bearer` **Query parameters:** - `mint` (string, required) — Token mint (base58). **Response:** pools[] each has pool_address, dex_type, quote_mint, program_id. pools_found = count; discover_ms = on-chain discovery latency. ### GET /v1/refund/scan **Scan wallet for reclaimable rent** — Scans the caller's per-user active wallet for empty token accounts (ATAs) and dust-balance accounts, reporting the SOL rent reclaimable from each. Read-only (no on-chain tx); resolves the wallet from the tenant's X-User-Ref context. `GET https://api.stryke.gg/v1/refund/scan` · auth: `Authorization: Bearer` **Response:** empty_accounts = zero-balance ATAs closeable immediately; dust_accounts = non-zero token balances that need burn-then-close. *_rent_sol are human-formatted strings; total_reclaimable_lamports is the raw u64. token_program distinguishes SPL Token vs Token-2022. ### POST /v1/refund/close-empty **Close empty token accounts** — Scans then closes all empty (zero-balance) token accounts for the caller's wallet in a single batch on-chain transaction, reclaiming their SOL rent. Idempotency-key supported (trade-scope idempotency layer). Frozen (honeypot) accounts are skipped and reported. `POST https://api.stryke.gg/v1/refund/close-empty` · auth: `Authorization: Bearer` **Response:** success = true when closed>0 OR (no failures and no frozen). closed/failed/frozen are counts; reclaimed_lamports/_sol is rent recovered; signature is the close tx (null if none sent). If frozen>0, error explains the account is FROZEN (honeypot). When nothing to close, returns {success:true,message:"No empty token accounts found",closed:0,reclaimed_sol:"0"}. > Supports Idempotency-Key header (trade-scope idempotency layer). ### POST /v1/refund/burn-and-close **Burn dust and close accounts** — Scans for token accounts holding dust balances, burns the remaining tokens, and closes the accounts to reclaim SOL rent — in one on-chain batch. Optional max_accounts caps how many are processed this call. Frozen accounts are skipped and reported. `POST https://api.stryke.gg/v1/refund/burn-and-close` · auth: `Authorization: Bearer` **Body parameters:** - `max_accounts` (int, optional) — Maximum number of dust accounts to burn+close in this call. Defaults to all if omitted. **Response:** success = true when closed>0 OR (no failures and no frozen). total_dust_found = number of dust accounts attempted (after max_accounts cap). signature is the burn+close tx (null if none). If frozen>0, error notes the account is FROZEN (honeypot). When no dust found, returns {success:true,message:"No dust token accounts found",closed:0,reclaimed_sol:"0"}. > Supports Idempotency-Key header (trade-scope idempotency layer). ### POST /v1/refund/close-nonce **Close nonce accounts** — Closes durable-nonce accounts for the caller's wallet and reclaims their rent. Pass specific nonce_pubkeys to close those, or omit/empty to close ALL nonce (pro) accounts looked up from the DB for this wallet. Closes in one on-chain batch. `POST https://api.stryke.gg/v1/refund/close-nonce` · auth: `Authorization: Bearer` **Body parameters:** - `nonce_pubkeys` (string[], optional) — Specific nonce account pubkeys to close. If omitted or empty, closes ALL nonce accounts for this wallet (resolved from DB pro_accounts). **Response:** closed/failed are counts; reclaimed_lamports/_sol is rent recovered; signature is the batch close tx (null if none); total_nonce_accounts is how many were targeted. When there are no nonce accounts to close, returns {success:true,message:"No nonce accounts to close",closed:0,reclaimed_sol:"0"}. > Supports Idempotency-Key header (trade-scope idempotency layer). ## Trading API: Wallets Auth: send your key as `Authorization: Bearer ` (the trading API uses Bearer, NOT the X-API-Key header used by the data API). Custodial trading wallets — create, import, list, rename, switch, withdraw, and migrate. wallet scope; per-tenant. ### GET /v1/wallet/list **List wallets** — Lists all wallets owned by the requesting tenant user (resolved from X-User-Ref). Returns each wallet's id, name, public key, active flag, generated flag, and migration timestamp. `GET https://api.stryke.gg/v1/wallet/list` · auth: `Authorization: Bearer` **Response:** data is an array of wallet objects. is_generated=true means the keypair was created by the API (not imported). migrated_at is an RFC3339 string or null. Wallets are scoped to the caller's tenant user. ### POST /v1/wallet/create **Create wallet** — Generates a brand-new Solana keypair server-side, stores it encrypted, and associates it with the tenant user. Optionally makes it the active wallet. `POST https://api.stryke.gg/v1/wallet/create` · auth: `Authorization: Bearer` **Body parameters:** - `name` (string, optional) — Display name for the wallet. Defaults to "Wallet" if omitted. - `set_active` (bool, optional) — If true, marks the new wallet as the user's active wallet. Defaults to false. **Response:** data.id is the new wallet row id; public_key is the generated address. The private key is never returned. Supports Idempotency-Key header to dedupe retries. > Idempotency-Key header supported (wallet routes pass through the idempotency layer). ### POST /v1/wallet/import **Import wallet** — Imports an existing wallet from a base58-encoded 64-byte secret key, stores it encrypted, and associates it with the tenant user. Key is validated without panicking; malformed keys are rejected. `POST https://api.stryke.gg/v1/wallet/import` · auth: `Authorization: Bearer` **Body parameters:** - `private_key` (string, required) — Base58-encoded full 64-byte secret key. - `name` (string, optional) — Display name for the wallet. Defaults to "Imported Wallet" if omitted. - `set_active` (bool, optional) — If true, marks the imported wallet as active. Defaults to false. **Response:** data.id is the new wallet row id; public_key is derived from the imported key. The secret is never echoed back. > Idempotency-Key header supported. ### POST /v1/wallet/rename **Rename wallet** — Renames an existing wallet owned by the tenant user. The rename is ownership-scoped — only wallets belonging to the caller can be renamed. `POST https://api.stryke.gg/v1/wallet/rename` · auth: `Authorization: Bearer` **Body parameters:** - `wallet_id` (int, required) — ID of the wallet to rename (must belong to the caller). - `name` (string, required) — New display name. Cannot be empty/whitespace. **Response:** Echoes the wallet_id and new name on success. > Idempotency-Key header supported. ### POST /v1/wallet/switch **Switch active wallet** — Sets the given wallet as the tenant user's active wallet. Scoped to the caller — only the user's own wallets can be activated. `POST https://api.stryke.gg/v1/wallet/switch` · auth: `Authorization: Bearer` **Body parameters:** - `wallet_id` (int, required) — ID of the wallet to make active (must belong to the caller). **Response:** data.active_wallet_id confirms the newly active wallet. > Idempotency-Key header supported. ### POST /v1/wallet/withdraw **Withdraw SOL** — Sends SOL from the user's active wallet to an external address on-chain. Validates the destination, amount bounds, active-wallet presence, and self-send. Requires an executor/RPC deployment. `POST https://api.stryke.gg/v1/wallet/withdraw` · auth: `Authorization: Bearer` **Body parameters:** - `to_address` (string, required) — Destination Solana address (base58 pubkey). - `amount_sol` (number, required) — Amount of SOL to send. Must be > 0 and ≤ 1,000,000. **Response:** On success, data.signature is the on-chain transfer signature; from is the active wallet pubkey. The send is executed from the caller's active wallet keypair, loaded server-side. > Idempotency-Key header supported (strongly recommended for on-chain sends). Returns 503 on DB-only deployments lacking an executor/RPC. ### DELETE /v1/wallet/{id} **Delete wallet** — Deletes a wallet by ID for the tenant user. Refuses to delete the user's last remaining wallet. `DELETE https://api.stryke.gg/v1/wallet/{id}` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — ID of the wallet to delete (must belong to the caller). **Response:** On success, data.deleted_wallet_id echoes the removed wallet id. Deletion is ownership-scoped to the caller. > Idempotency-Key header supported. ### POST /v1/wallet/migrate **Migrate wallet from legacy app** — Imports a wallet from a legacy app's base58 64-byte secret key, stores it encrypted, and stamps migrated_at=NOW() plus source_app_id (the calling API app) so the terminal can track migrations. Optionally sets it active. `POST https://api.stryke.gg/v1/wallet/migrate` · auth: `Authorization: Bearer` **Body parameters:** - `secret_key_base58` (string, required) — Base58-encoded full 64-byte secret key from the legacy app. - `name` (string, required) — Display name for the migrated wallet. - `set_active` (bool, optional) — If true, atomically makes this the user's active wallet. Defaults to false. **Response:** data.migrated is always true on success; the wallet row is stamped with migrated_at and source_app_id (resolved from the authenticated app, AuthCtx). is_generated is stored as FALSE. > Idempotency-Key header supported. source_app_id is taken from the authenticated API app, not from the request body. ## Trading API: Keys & Sessions Auth: send your key as `Authorization: Bearer ` (the trading API uses Bearer, NOT the X-API-Key header used by the data API). Self-serve API key issuance and wallet sessions. Public surface: a Solana wallet signature is the auth (no bearer key required). ### POST /v1/keys/challenge **Request key-issuance challenge** — Mints a single-use, TTL-bound nonce and a human-readable message for a Solana wallet to sign (Phantom signMessage). The signed message later proves wallet ownership for self-serve key issuance/login. Public — no bearer auth; the wallet signature is the auth. Per-IP rate limited and optionally allowlist-gated. `POST https://api.stryke.gg/v1/keys/challenge` · public **Body parameters:** - `wallet` (string, required) — Base58 Solana wallet address to issue/own the key. **Response:** data.nonce + data.message are echoed back; the client must sign the EXACT message bytes. data.expires_in_secs is the challenge TTL (a platform setting, default 600). The nonce is NOT consumed here — only on a successful issue/login. > Public surface — gated by a platform setting. Per-IP token bucket (a platform setting / a platform setting) using X-Real-IP (preferred) or X-Forwarded-For. ### POST /v1/keys/issue **Issue (or rotate) API key via signed challenge** — Verifies the ed25519 signature over the exact challenge message, atomically consumes the single-use nonce, then issues a fresh API key bound to the wallet — or rotates the wallet's existing key (one key per wallet). The full key is returned ONCE; an encrypted recoverable copy is stored so it can be re-revealed later via the session. Also returns a wallet session token. Public — the wallet signature is the auth. `POST https://api.stryke.gg/v1/keys/issue` · public **Body parameters:** - `wallet` (string, required) — Base58 Solana wallet address; must match the challenge's wallet. - `nonce` (string, required) — Nonce returned by /v1/keys/challenge. - `signature` (string, required) — ed25519 signature over the challenge message — base64 (preferred) or base58. **Response:** data.key is the full secret, shown ONCE. rotated=false means newly created, true means an existing key was rotated. scopes/tier/credits_monthly/rate_per_min/fee_bps come from SelfServeConfig env. session_token (HMAC-signed, default 7-day TTL) lets the wallet reveal/reset without re-signing; session_expires_at is a unix-seconds timestamp. > Public surface — gated by a platform setting. Signature is verified BEFORE the nonce is consumed (a forged sig cannot burn a pending challenge); the nonce is consumed atomically only after verify (single-use, replay-safe). ### POST /v1/keys/login **Wallet login (start session)** — Proves wallet ownership once via a signed challenge and returns a session token (default 7 days) without issuing a key. Reports whether the wallet already has a key and whether it is revealable, so a portal can show Reveal/Reset vs Generate. Public — the wallet signature is the auth. `POST https://api.stryke.gg/v1/keys/login` · public **Body parameters:** - `wallet` (string, required) — Base58 Solana wallet address; must match the challenge's wallet. - `nonce` (string, required) — Nonce returned by /v1/keys/challenge. - `signature` (string, required) — ed25519 signature over the challenge message — base64 or base58. **Response:** session_token (HMAC-signed, a platform setting, default 7d) + session_expires_at (unix secs). has_key = wallet already has an enabled key. key_prefix is null if none. revealable = an encrypted copy exists AND the key is enabled (older keys predating self-reveal are not revealable). > Public surface — gated by a platform setting. Consumes the nonce (single-use) after verifying the signature. ### POST /v1/keys/reveal **Reveal full API key (session)** — Returns the FULL API key for the session wallet, decrypted server-side from the stored recoverable copy. Requires a valid session token (obtained via /v1/keys/issue or /v1/keys/login) — no new signature needed. Public route; the session token is the auth. `POST https://api.stryke.gg/v1/keys/reveal` · public **Body parameters:** - `session_token` (string, required) — Wallet session token from /v1/keys/login or /v1/keys/issue. **Response:** data.key is the decrypted full secret; key_prefix is its public prefix. > Public surface. Session token is HMAC-verified (constant-time) against the engine pepper with embedded expiry. ### POST /v1/keys/reset **Reset (rotate) API key (session)** — Rotates — or first-time generates — the session wallet's API key. The previous key stops working immediately and a fresh full key is returned once. Requires a valid session token; no new signature needed. Public route; the session token is the auth. `POST https://api.stryke.gg/v1/keys/reset` · public **Body parameters:** - `session_token` (string, required) — Wallet session token from /v1/keys/login or /v1/keys/issue. **Response:** data.key is the new full secret, shown once. rotated=true if a key existed before (false on first-time generate). scopes/tier/credits_monthly/rate_per_min/fee_bps reflect the issued app config. No new session_token is returned (the existing session stays valid). > Public surface. Session token is HMAC-verified against the engine pepper. Old key is invalidated immediately on rotate. ## Trading API: Billing & Tiers Auth: send your key as `Authorization: Bearer ` (the trading API uses Bearer, NOT the X-API-Key header used by the data API). Paid tiers with live SOL quotes, on-chain SOL payment redemption (anti-replay), and stake checks. ### GET /v1/tiers **List paid tiers** — Public pricing list: every tier from the operator-editable api_tiers table with USD price, a live SOL-lamports quote (price_usd / live SOL price), credits, rate limits, stake eligibility, plus the payment treasury address. Drives the pricing page so a client can build the exact SOL payment. `GET https://api.stryke.gg/v1/tiers` · public **Response:** data.tiers[]: name (tier id), display_name, price_usd, price_sol_lamports (null when price is 0 or SOL price unavailable), credits_monthly (null = unlimited), unlimited (credits_monthly is null), rate_per_min, stake_min (raw $STRYKE units), stake_eligible (stake_min > 0), window_days. Top level: pay_currency always "sol", sol_price_usd (null if unavailable), treasury (null if unconfigured), stake_rail_enabled (whether STRYKE_MINT is set). > Fully public — no auth, no session. Pricing is DB-driven (no rebuild to change). ### POST /v1/billing/status **Billing status for session key** — Returns the current tier, credit quota/usage, rate limit, and tier expiry for the API key owned by the wallet behind the session token. Resolves session -> wallet -> self-serve app id in-handler. `POST https://api.stryke.gg/v1/billing/status` · public **Body parameters:** - `session_token` (string, required) — Wallet session token from POST /v1/keys/login. Verified server-side (HMAC) to resolve the owner wallet and its self-serve API key. **Response:** data.tier (nullable), credits_monthly (null = unlimited), credits_used, credits_remaining (null when unlimited; else max(monthly-used,0)), unlimited (credits_monthly is null), rate_per_min, tier_expires_at (RFC3339, null if no expiry). > Auth is the wallet signature session, not an X-API-Key. Generate a key via /v1/keys/* first. ### POST /v1/billing/pay **Pay (SOL) and apply tier** — Verifies an on-chain SOL payment and applies the chosen paid tier to the session wallet's API key. Confirms the tx succeeded, was sent from the session wallet (fee payer / account index 0), and credited the treasury >= the tier price (priced in USD, converted at live SOL price with a drift tolerance). Records the signature single-use (atomic redeem) so it can't be replayed. `POST https://api.stryke.gg/v1/billing/pay` · public **Body parameters:** - `session_token` (string, required) — Wallet session token from POST /v1/keys/login; binds the payment to the owner wallet. - `tier` (string, required) — Tier name to purchase (must be a paid tier with price_usd > 0). - `currency` (string, optional) — Payment currency. Defaults to "sol"; only "sol" is accepted (USDC reserved for later). - `tx_signature` (string, required) — Signature of the on-chain SOL transfer to the treasury. Must be confirmed, successful, sent from the session wallet, and credit the treasury >= the (tolerance-adjusted) tier price. **Response:** data.tier (applied tier name), credits_monthly (null = unlimited), rate_per_min, tier_expires_at (RFC3339, end of tier window), paid_lamports (lamports the treasury actually received in the verified tx). > Signature recording + tier apply are one atomic DB tx (failed apply rolls back, no burned signature). Drift tolerance from BILLING_SOL_TOLERANCE_BPS (default 300bps, clamped <= 5000bps). ### POST /v1/billing/stake-check **Stake-check ($STRYKE) tier grant** — Checks the session wallet's $STRYKE balance (held in its ATA) and grants the highest-priced tier whose stake_min it meets. Re-checkable; does not reset the credit window unless the tier actually changes, and the tier lapses at window end if the holder drops below the threshold. `POST https://api.stryke.gg/v1/billing/stake-check` · public **Body parameters:** - `session_token` (string, required) — Wallet session token from POST /v1/keys/login; identifies the wallet whose $STRYKE balance is checked. **Response:** data.tier (granted tier name), stryke_balance (raw $STRYKE units in the wallet's ATA), credits_monthly (null = unlimited), rate_per_min, tier_expires_at (RFC3339, end of window). Each grant is audited with a synthetic stake::: signature. > Re-checks call apply_tier with force_reset=false so spamming stake-check cannot zero credits_used. Only counts ATA-held $STRYKE. ## Trading API: Token Verification Auth: send your key as `Authorization: Bearer ` (the trading API uses Bearer, NOT the X-API-Key header used by the data API). The paid 'Verified by Stryke' token badge — status reads (public) and badge purchase. ### GET /v1/tokens/verified **List verified tokens** — Public list of all mints that currently hold an active paid 'Verified by Stryke' badge. Reads the verified-token table directly (the deepscan engine uses this as a warm cache); always works regardless of whether badge granting is enabled. `GET https://api.stryke.gg/v1/tokens/verified` · public **Response:** data.verified[] = active verified mints; each has mint, since (RFC3339 verified_at), score_at_verify (Deep Scan score at grant time). data.count = number of entries. > Public read — no X-API-Key or wallet session needed. Reports the table even when a platform setting is off. ### GET /v1/tokens/{mint}/verification **Get token verification status** — Public per-token verification lookup. Returns whether a single mint holds an active 'Verified by Stryke' badge; the deepscan engine polls this (fail-open, cached) to decide whether to render the badge. Score-neutral — never affects the token's safety score. `GET https://api.stryke.gg/v1/tokens/{mint}/verification` · public **Path parameters:** - `mint` (string, optional) — Token mint address to look up (trimmed). **Response:** When active: verified=true, status="active", mint, since (RFC3339), score_at_verify. When not verified (no row or non-active): verified=false, status="none", mint only. > Public read — no auth. Returns verified=false/status=none for unknown or inactive mints rather than 404. ### POST /v1/tokens/verify **Verify token (grant badge)** — Wallet-session + on-chain SOL payment + Deep Scan score gate that grants a paid 'Verified by Stryke' badge to a mint. Requires a valid HMAC wallet session, a confirmed SOL payment from that wallet to the verify/billing treasury (live SOL pricing with drift tolerance), and a current Deep Scan score >= the configured bar. Atomic grant with anti-replay (one tx signature can verify exactly one token). Revenue routes to the trEnD treasury. `POST https://api.stryke.gg/v1/tokens/verify` · public **Body parameters:** - `session_token` (string, required) — HMAC wallet session token from the keys/billing wallet-sig flow; identifies the paying wallet. - `mint` (string, required) — Token mint address to verify (must be a valid pubkey). - `tx_signature` (string, required) — Signature of the on-chain SOL payment tx (payer must equal the session wallet; treasury must receive >= the SOL-priced fee minus tolerance). Single-use. **Response:** On success: verified=true, mint, score_at_verify (Deep Scan score that cleared the gate), paid_lamports (lamports the treasury actually received). Errors use {success:false, error:{code,message}}. > Entire grant path is gated behind a platform setting (default OFF) — ships inert until the operator enables it after on-chain testing. Badge is score-neutral: it never alters the token's Deep Scan score/grade/flags. Auto-revoked later by a periodic sweep if the score drops below the bar. Operator knobs (env): VERIFY_MIN_SCORE (default 80), VERIFY_PRICE_USD, VERIFY_SOL_TOLERANCE_BPS (default 300, max 5000), VERIFY_TREASURY (else billing treasury), a platform setting (default 55), the platform config (score-gate auth; fails closed if unset). ## Trading API: Quests Auth: send your key as `Authorization: Bearer ` (the trading API uses Bearer, NOT the X-API-Key header used by the data API). Multi-tenant quest / engagement campaigns, tasks, verification, status, and leaderboards. quests scope. ### POST /v1/quests/projects **Create quest project** — Creates a quest project in the Node quests engine. Thin metered proxy: the Rust layer authenticates the app key + `quests` scope, charges credits, then forwards the body verbatim with the authenticated `app.id` as `x-stryke-app-id`, which the engine uses to enforce per-tenant ownership (a project's `stryke_app_id` must match the calling app). `POST https://api.stryke.gg/v1/quests/projects` · auth: `Authorization: Bearer` **Body parameters:** - `` (object, required) — Free-form JSON forwarded verbatim to the quests engine's POST /quests/projects (e.g. project name/config). Body shape is owned by the Node quests-engine, not this proxy. **Response:** 2xx body is the quests-engine's JSON forwarded verbatim (Content-Type application/json); field shape is owned by the engine. The `success` boolean drives credit settlement. On proxy faults the Rust layer returns its own envelope: `{"success":false,"error":{"code":...,"message":...}}`. > Metered proxy to quests-engine. Credit cost = cost_read (default 1). Tenancy enforced engine-side via forwarded x-stryke-app-id. ### POST /v1/quests/projects/{pid}/credentials **Set project credentials** — Stores per-project verification credentials (e.g. X/Reddit/Discord API secrets) for project `pid` in the quests engine's encrypted vault. Forwards the body verbatim with the authenticated app id; engine rejects if `pid` is not owned by the calling app. `POST https://api.stryke.gg/v1/quests/projects/{pid}/credentials` · auth: `Authorization: Bearer` **Path parameters:** - `pid` (int, optional) — Quest project id (i64). **Body parameters:** - `` (object, required) — Free-form JSON of provider credentials forwarded verbatim to POST /quests/projects/{pid}/credentials. Shape owned by the quests-engine. **Response:** 2xx body forwarded verbatim from the quests-engine (application/json). Proxy faults use `{"success":false,"error":{"code":...}}`. > Credit cost = cost_read (default 1). Credentials stored in the engine's AES vault, never returned in plaintext. ### GET /v1/quests/projects/{pid}/credentials **Get project credentials** — Returns the (redacted) credential configuration for project `pid` from the quests engine. Proxy forwards the authenticated app id; engine enforces project ownership. `GET https://api.stryke.gg/v1/quests/projects/{pid}/credentials` · auth: `Authorization: Bearer` **Path parameters:** - `pid` (int, optional) — Quest project id (i64). **Response:** 2xx body forwarded verbatim from the quests-engine; secrets are redacted engine-side. Proxy faults use the `{"success":false,"error":{...}}` envelope. > Credit cost = cost_read (default 1). ### POST /v1/quests/campaigns **Create campaign** — Creates a campaign (a grouping of tasks under a project) in the quests engine. Body forwarded verbatim with the authenticated app id for per-tenant ownership. `POST https://api.stryke.gg/v1/quests/campaigns` · auth: `Authorization: Bearer` **Body parameters:** - `` (object, required) — Free-form JSON forwarded verbatim to POST /quests/campaigns (e.g. project_id, name, schedule). Shape owned by the quests-engine. **Response:** 2xx body forwarded verbatim from the quests-engine (application/json). Proxy faults use `{"success":false,"error":{...}}`. > Credit cost = cost_read (default 1). ### POST /v1/quests/tasks **Create task** — Creates a verifiable task (e.g. follow/retweet/join/on-chain action) under a campaign in the quests engine. Body forwarded verbatim with the authenticated app id. `POST https://api.stryke.gg/v1/quests/tasks` · auth: `Authorization: Bearer` **Body parameters:** - `` (object, required) — Free-form JSON forwarded verbatim to POST /quests/tasks (e.g. campaign_id, type, target, reward). Shape owned by the quests-engine. **Response:** 2xx body forwarded verbatim from the quests-engine (application/json). Proxy faults use `{"success":false,"error":{...}}`. > Credit cost = cost_read (default 1). ### GET /v1/quests/tasks **List tasks** — Lists tasks from the quests engine, scoped to the calling app. All query-string params are passed through verbatim (percent-encoded) to the engine's GET /quests/tasks. `GET https://api.stryke.gg/v1/quests/tasks` · auth: `Authorization: Bearer` **Query parameters:** - `` (string, optional) — Arbitrary filter params (e.g. campaign_id, project_id, status) forwarded verbatim to the quests-engine. Accepted params are defined by the engine. **Response:** 2xx body forwarded verbatim from the quests-engine (application/json). Proxy faults use `{"success":false,"error":{...}}`. > Credit cost = cost_read (default 1). ### POST /v1/quests/verify **Verify quest task** — Submits a task completion attempt for verification (the billable unit of the Quests API). The engine performs the actual check (X/Reddit/Discord/on-chain). Charge-per-attempt: the credit reservation is kept on any 2xx success:true, including verified:false, since the upstream cost was incurred. `POST https://api.stryke.gg/v1/quests/verify` · auth: `Authorization: Bearer` **Body parameters:** - `` (object, required) — Free-form JSON forwarded verbatim to POST /quests/verify (e.g. task_id, wallet/user identity, proof). Shape owned by the quests-engine. **Response:** 2xx body forwarded verbatim from the quests-engine; `verified` reflects whether the task passed. Treated as an EXECUTION path: a 2xx with success:true keeps the credit charge even when verified:false. Proxy faults use `{"success":false,"error":{...}}`. > Credit cost = cost_quests_verify (env CREDIT_COST_QUESTS_VERIFY, default 2). Billable per-attempt; the charge is retained on any 2xx success:true response. ### GET /v1/quests/status **Quest status** — Returns task/campaign completion status for a participant from the quests engine, scoped to the calling app. Query params forwarded verbatim. `GET https://api.stryke.gg/v1/quests/status` · auth: `Authorization: Bearer` **Query parameters:** - `` (string, optional) — Arbitrary params (e.g. wallet/user id, campaign_id, task_id) forwarded verbatim to the quests-engine's GET /quests/status. **Response:** 2xx body forwarded verbatim from the quests-engine (application/json). Proxy faults use `{"success":false,"error":{...}}`. > Credit cost = cost_read (default 1). ### GET /v1/quests/leaderboard **Quest leaderboard** — Returns the participant leaderboard for a campaign/project from the quests engine, scoped to the calling app. Query params forwarded verbatim. `GET https://api.stryke.gg/v1/quests/leaderboard` · auth: `Authorization: Bearer` **Query parameters:** - `` (string, optional) — Arbitrary params (e.g. campaign_id, project_id, limit) forwarded verbatim to the quests-engine's GET /quests/leaderboard. **Response:** 2xx body forwarded verbatim from the quests-engine (application/json). Proxy faults use `{"success":false,"error":{...}}`. > Credit cost = cost_read (default 1). ### GET /v1/quests/usage **Quests usage** — Returns the quests engine's per-app usage/metering snapshot (verify counts, etc.), scoped to the calling app. Free — not credit-charged (paths ending in /usage cost 0). Query params forwarded verbatim. `GET https://api.stryke.gg/v1/quests/usage` · auth: `Authorization: Bearer` **Query parameters:** - `` (string, optional) — Arbitrary params (e.g. window/period) forwarded verbatim to the quests-engine's GET /quests/usage. **Response:** 2xx body forwarded verbatim from the quests-engine (application/json). Proxy faults use `{"success":false,"error":{...}}`. > Credit cost = 0 (free; cost_for_path returns 0 for paths ending in /usage). ## Trading API: Account & Profile Auth: send your key as `Authorization: Bearer ` (the trading API uses Bearer, NOT the X-API-Key header used by the data API). User settings, rewards/XP, invite codes, and support tickets. read scope; per-tenant. ### GET /v1/settings **Get user settings** — Returns the full settings dump for the tenant user resolved from the X-User-Ref header (slippage, gas, MEV flags, quick buy/sell presets, PNL card prefs, withdraw address). Per-tenant: the user is resolved from X-User-Ref scoped to the calling app. `GET https://api.stryke.gg/v1/settings` · auth: `Authorization: Bearer` **Response:** settings is the UserProfile row: locale; default_buy_amount + default_slippage_bps; buy_amounts (5 lamport presets) + buy_slippage_bps; sell_percents (4 pct presets) + sell_slippage_bps; priority_fee_lamports, gas_cu_limit, gas_cu_price, gas_tip; confirm_trades, mev_protect_buy, mev_protect_sell, sell_protection; pnl_cards_* prefs; autosell_profile_id (nullable); withdraw_address (nullable) + withdraw_address_locked; created_at + last_active timestamps. On the not-found path returns {"success":false,"error":"User not found. Send any trade first to auto-create."}. > Requires the X-User-Ref header to identify the tenant user (the user row is auto-created on first trade; until then GET returns success:false 'User not found'). ### PUT /v1/settings **Update user settings (batch)** — Batch-updates any subset of user settings — only fields present in the JSON body are applied, omitted fields are left unchanged. Each field is validated and written independently; the response reports which fields were updated and any per-field errors. Per-tenant: user resolved from X-User-Ref. `PUT https://api.stryke.gg/v1/settings` · auth: `Authorization: Bearer` **Body parameters:** - `locale` (string, optional) — UI locale. Must be one of en, ru, zh-CN, es. - `buy_slippage_bps` (int, optional) — Buy slippage in bps, 0-10000. - `sell_slippage_bps` (int, optional) — Sell slippage in bps, 0-10000. - `priority_fee_lamports` (int, optional) — Priority fee in lamports, must be >= 0. - `gas_cu_price` (int, optional) — Compute-unit price, must be >= 0. - `gas_cu_limit` (int, optional) — Compute-unit limit, must be >= 0 (written via raw SQL). - `gas_tip` (number, optional) — Gas tip in SOL, must be >= 0 (written via raw SQL). - `mev_protect_buy` (bool, optional) — Enable MEV protection on buys. - `mev_protect_sell` (bool, optional) — Enable MEV protection on sells. - `sell_protection` (bool, optional) — Enable sell protection. - `confirm_trades` (bool, optional) — Require trade confirmation dialog. - `pnl_cards_enabled` (bool, optional) — Enable PNL cards. - `pnl_cards_hide_losses` (bool, optional) — Hide losing trades on PNL cards. - `pnl_cards_hide_amounts` (bool, optional) — Hide amounts on PNL cards. - `pnl_cards_show_qr` (bool, optional) — Show QR code on PNL cards. - `autosell_profile_id` (int|null, optional) — Autosell profile id. Nested-optional: omit = skip, null = clear, value = set. - `withdraw_address` (string|null, optional) — Withdraw destination. Nested-optional: omit = skip, null or empty string = clear, value = set. **Response:** On full success: {"success":true,"updated":[]}. If any field failed validation or its DB write errored: {"success":false,"updated":[],"errors":[]} — partial writes are NOT rolled back (already-applied fields persist). Validation errors include 'Invalid locale...', 'buy_slippage_bps must be 0-10000', 'priority_fee_lamports must be >= 0', etc. > Requires X-User-Ref header (user resolved from RequestCtx). autosell_profile_id and withdraw_address use nested-optional semantics (omit vs null vs value). ### PUT /v1/settings/buy-amounts **Set quick-buy amounts** — Sets the 5 quick-buy preset amounts (in lamports) for the tenant user. Requires exactly 5 positive values; writes them to buy_amount_1..5 (1-based index). Per-tenant: user resolved from X-User-Ref. `PUT https://api.stryke.gg/v1/settings/buy-amounts` · auth: `Authorization: Bearer` **Body parameters:** - `amounts` (int[], required) — Exactly 5 buy amounts in lamports, each > 0. **Response:** On success echoes back the saved buy_amounts array. On validation/DB failure returns {"success":false,"error":""} (e.g. 'Expected exactly 5 buy amounts in lamports', 'Amount at index N must be > 0', 'Failed to set buy_amount_N:...'). > Requires X-User-Ref header. ### PUT /v1/settings/sell-percents **Set quick-sell percentages** — Sets the 4 quick-sell preset percentages for the tenant user. Requires exactly 4 values each in 1-100; writes them to sell_percent_1..4 (1-based index). Per-tenant: user resolved from X-User-Ref. `PUT https://api.stryke.gg/v1/settings/sell-percents` · auth: `Authorization: Bearer` **Body parameters:** - `percents` (int[], required) — Exactly 4 sell percentages, each 1-100. **Response:** On success echoes back the saved sell_percents array. On validation/DB failure returns {"success":false,"error":""} (e.g. 'Expected exactly 4 sell percentages', 'Percent at index N must be 1-100', 'Failed to set sell_percent_N:...'). > Requires X-User-Ref header. ### POST /v1/settings/reset-buy **Reset buy settings** — Resets the tenant user's buy settings (quick-buy amounts, default buy amount, buy slippage) to system defaults. Per-tenant: user resolved from X-User-Ref. `POST https://api.stryke.gg/v1/settings/reset-buy` · auth: `Authorization: Bearer` **Response:** On success returns the applied defaults: buy_amounts [0.1, 0.25, 0.5, 1.0, 2.5 SOL in lamports], default_buy_amount 500000000, buy_slippage_bps 1500. On failure returns {"success":false,"error":"Failed to reset buy settings:..."}. > Requires X-User-Ref header (read via require_user from request extensions). No request body. ### POST /v1/settings/reset-sell **Reset sell settings** — Resets the tenant user's sell settings (quick-sell percentages and sell slippage) to system defaults. Per-tenant: user resolved from X-User-Ref. `POST https://api.stryke.gg/v1/settings/reset-sell` · auth: `Authorization: Bearer` **Response:** On success returns the applied defaults: sell_percents [25,50,75,100], sell_slippage_bps 1500. On failure returns {"success":false,"error":"Failed to reset sell settings:..."}. > Requires X-User-Ref header (read via require_user from request extensions). No request body. ### GET /v1/rewards/profile **Get rewards profile** — Returns the calling user's XP, level, daily streak, referral code, and unclaimed cashback/referral balances, plus computed progress toward the next level. Scoped to the authenticated user (resolved from X-User-Ref); auto-creates a rewards row on first access. `GET https://api.stryke.gg/v1/rewards/profile` · auth: `Authorization: Bearer` **Response:** data.current_xp/total_xp_earned = XP counters; level = current_level; streak = daily_streak; last_trade_at = last trade date (string or null); referral_code = user's invite code; cashback_balance_lamports/referral_balance_lamports = unclaimed amounts in lamports; next_level_xp = XP threshold for next level (0 at max level); progress_pct = 0-100 progress within current level. > Requires X-User-Ref tenancy header (attach_request_ctx). On any DB/user error returns the standard error_response JSON envelope ({success:false,error:...}). ### GET /v1/rewards/achievements **List achievements** — Returns the full achievement catalog merged with the calling user's unlock status and unlock timestamps. Scoped to the authenticated user (resolved from X-User-Ref). `GET https://api.stryke.gg/v1/rewards/achievements` · auth: `Authorization: Bearer` **Response:** data.total = catalog size; data.unlocked = count unlocked by user; achievements[] = every achievement with id/name/description/icon/xp_reward/category plus unlocked (bool) and unlocked_at (unix epoch i64 or null). > Requires X-User-Ref tenancy header. DB/user errors return the error_response JSON envelope. ### GET /v1/rewards/referral-stats **Get referral stats** — Returns the calling user's multi-level referral tree stats (per-depth commission earned and user counts) plus the direct (depth-1) referral count. Scoped to the authenticated user. `GET https://api.stryke.gg/v1/rewards/referral-stats` · auth: `Authorization: Bearer` **Response:** data.direct_referrals = count of direct referrals; stats[] = per referral-tree depth: depth (level below user), total_commission_lamports (sum earned at that depth, lamports), total_trades (user_count at that depth). > Requires X-User-Ref tenancy header. DB/user errors return the error_response JSON envelope. ### GET /v1/rewards/leaderboard **XP leaderboard** — Returns the top users ranked by XP descending. PII-safe: raw telegram_id is never exposed — each entry gets a rank-derived anon handle ("anon-1", "anon-2",...). App-wide (not per-user). `GET https://api.stryke.gg/v1/rewards/leaderboard` · auth: `Authorization: Bearer` **Query parameters:** - `limit` (int, optional) — Max entries to return; defaults to 10, capped at 100. **Response:** data.leaderboard[] sorted by xp DESC: rank (1-based), handle (rank-derived anon-N, NOT a resolvable id — v0.7.265 PII fix), xp (current_xp), level (current_level), streak (daily_streak). > No X-User-Ref required — global leaderboard. Does not call require_user. DB errors return the error_response JSON envelope. ### GET /v1/rewards/levels **List level definitions** — Returns all reward level definitions with their XP thresholds and names. Static catalog — no user context. `GET https://api.stryke.gg/v1/rewards/levels` · auth: `Authorization: Bearer` **Response:** data[] = each level: level (number), xp_required (cumulative XP threshold to reach it), name (level title). > No X-User-Ref required — static level catalog. Does not call require_user. DB errors return the error_response JSON envelope. ### POST /v1/invite/create **Create invite code** — Creates a new invite/referral code owned by the calling tenant user (resolved from X-User-Ref via the request context). The code, optional label, max_uses (default 1), and optional expiry are persisted under created_by = caller. `POST https://api.stryke.gg/v1/invite/create` · auth: `Authorization: Bearer` **Body parameters:** - `code` (string, required) — The invite code string to create. Required and must be non-empty. - `label` (string, optional) — Optional human label for the code. - `max_uses` (int, optional) — Max redemptions allowed. Defaults to 1 if omitted. - `expires_at` (int, optional) — Optional Unix epoch (seconds) expiry timestamp. **Response:** data echoes the created code, label, max_uses, expires_at, and created_by (the caller's resolved user id). Errors return HTTP 200 with {"success":false,"error":"..."}: empty code -> 'code is required and cannot be empty'; DB failure -> 'Failed to create invite code:...'; unresolved user -> 'missing user'. > Application-level failures (empty code, missing user, DB error) are returned as HTTP 200 with success:false rather than a non-2xx status. ### GET /v1/invite/list **List my invite codes** — Lists the CALLER's invite codes with usage stats, scoped to created_by = caller (v0.7.221 fix; previously leaked every code system-wide). Returns each code with label, counts, active flag, and timestamps. `GET https://api.stryke.gg/v1/invite/list` · auth: `Authorization: Bearer` **Response:** data.total is the number of codes returned; data.codes[] each contains code, label, created_by, max_uses, use_count, is_active, expires_at, created_at. Only codes the caller created are returned. > App-level failures return HTTP 200 with success:false ('missing user' or 'Failed to list invite codes:...'). ### POST /v1/invite/use **Redeem invite code** — Validates and consumes an invite code for the calling user, then marks the user alpha-approved. Validation + consumption is atomic; on success the caller gains alpha access. `POST https://api.stryke.gg/v1/invite/use` · auth: `Authorization: Bearer` **Body parameters:** - `code` (string, required) — The invite code to redeem. Required and must be non-empty. **Response:** data echoes the redeemed code and alpha_approved:true. The caller's telegram_id is deliberately NOT echoed (v0.7.223 fix #12 — avoids a confirmation oracle). Errors (HTTP 200, success:false): empty code; 'Invalid, expired, or exhausted invite code' when validation fails; 'Code was consumed but failed to approve user:...' if approval fails post-consume; 'missing user'. > Invalid/expired/exhausted codes return HTTP 200 with success:false, not a 4xx. ### POST /v1/invite/revoke **Revoke invite code** — Deactivates one of the CALLER's invite codes, scoped via WHERE created_by = caller (v0.7.221 fix). Non-owners get a generic 'not found or already revoked' message regardless of whether the code exists. `POST https://api.stryke.gg/v1/invite/revoke` · auth: `Authorization: Bearer` **Body parameters:** - `code` (string, required) — The invite code to revoke. Required and must be non-empty. **Response:** On success data.revoked is true. If the code is not owned by the caller or was already revoked, returns HTTP 200 with {"success":false,"error":"Invite code not found or already revoked"}. Other errors: empty code; 'Failed to revoke invite code:...'; 'missing user'. > Not-found/not-owned returns HTTP 200 success:false (no 404). Ownership is enforced at the SQL layer. ### GET /v1/invite/alpha-users **List my codes' alpha redemptions** — Lists recent alpha-approved redemptions of the CALLER's invite codes only (SQL JOIN scoped to creator, v0.7.223 fix #5). telegram_id is stripped from every row (PII leak fix); only the redeeming code and timestamp are returned. `GET https://api.stryke.gg/v1/invite/alpha-users` · auth: `Authorization: Bearer` **Query parameters:** - `limit` (int, optional) — Max rows to return. Defaults to 20, capped at 500. **Response:** data.total is the row count; data.users[] each has invited_by_code and created_at only — telegram_id is intentionally omitted. Filtered to redemptions of codes the caller created; LIMIT applies after the ownership filter. > App-level failures return HTTP 200 with success:false ('missing user' or 'Failed to list alpha users:...'). ### GET /v1/invite/stats **My invite stats** — Returns aggregate invite stats scoped to the CALLER's codes only (v0.7.221 fix; previously exposed platform-wide user counts and the full code list). No platform-wide counts are exposed. `GET https://api.stryke.gg/v1/invite/stats` · auth: `Authorization: Bearer` **Response:** data.your_codes = total codes the caller created; active_codes = currently active among them; total_code_uses = sum of redemptions across the caller's codes. > App-level failures return HTTP 200 with success:false ('missing user' or 'DB error:...'). ### GET /v1/invite/check **Check alpha approval** — Checks whether the current user is alpha-approved. Resolves the user id from the request extensions (require_user) rather than the tenancy ctx. `GET https://api.stryke.gg/v1/invite/check` · auth: `Authorization: Bearer` **Response:** data.telegram_id is the caller's resolved user id and alpha_approved is the boolean approval status. Note: unlike the other invite routes, this one DOES echo telegram_id. > App-level failures return HTTP 200 with success:false ('missing user' or 'DB error:...'). ### POST /v1/tickets/create **Create support ticket** — Opens a new support ticket for the authenticated tenant user. The active wallet address (if any) is auto-resolved and attached; username is null under API-key auth. `POST https://api.stryke.gg/v1/tickets/create` · auth: `Authorization: Bearer` **Body parameters:** - `message` (string, required) — Ticket body. Must be non-empty (trimmed). **Response:** ticket_id is the new ticket's DB id; message echoes the submitted body. User is resolved from the tenancy RequestCtx (X-User-Ref). > Idempotency-Key header honored (read_routes does not apply the idempotency layer, but write effect is a single INSERT). user_id derived from tenancy context, not callable cross-user. ### GET /v1/tickets/list **List my tickets** — Returns the calling user's tickets, most recent first (capped at 10 by the DB layer). Scoped to the authenticated telegram_id; never returns other users' tickets. `GET https://api.stryke.gg/v1/tickets/list` · auth: `Authorization: Bearer` **Response:** count is the number of returned tickets; each ticket has id, message, status (open/replied/closed), admin_reply, admin_reply_at, created_at. ### GET /v1/tickets/stats **Ticket counts** — Returns the calling user's ticket counts: total plus per-status buckets (open, replied, closed). All counts are scoped to the caller's telegram_id (platform-wide totals were removed in v0.7.251 to prevent cross-tenant volume leakage). `GET https://api.stryke.gg/v1/tickets/stats` · auth: `Authorization: Bearer` **Response:** stats.total is the caller's total ticket count; open/replied/closed are per-status counts for the same user. ### GET /v1/tickets/{id} **Get ticket by id** — Fetches a single ticket by id, ownership-enforced at the SQL layer. A request for another user's ticket returns 'Ticket not found' identically to a nonexistent id (no existence leak). `GET https://api.stryke.gg/v1/tickets/{id}` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — Ticket id (i64). Must belong to the calling user. **Response:** ticket includes id, telegram_id, username, wallet_address, message, status, admin_reply, admin_reply_at, forwarded_message_id, created_at. ### POST /v1/tickets/{id}/close **Close ticket** — Closes one of the caller's tickets. Ownership is enforced in the WHERE clause; returns 'Ticket not found' for another user's id and 'already closed' if the caller's ticket is already inactive. `POST https://api.stryke.gg/v1/tickets/{id}/close` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — Ticket id (i64) to close. Must belong to the calling user. **Response:** On success returns the closed ticket_id and status 'closed'. > The former POST /v1/tickets/{id}/reply route was REMOVED in v0.7.220 (admin-reply spoofing vector) and is not in the route table. ## Trading API: Fees, PnL & Positions Auth: send your key as `Authorization: Bearer ` (the trading API uses Bearer, NOT the X-API-Key header used by the data API). Fee accounting, live and historical PnL, and position/trade history. read scope; per-tenant. ### GET /v1/fees/summary **Fee summary** — Returns the calling app's total platform fees collected with a buy/sell breakdown and effective fee rate. Scoped per-tenant via api_app_users.app_id (platform-wide totals are not exposed). `GET https://api.stryke.gg/v1/fees/summary` · auth: `Authorization: Bearer` **Response:** data.grand_total_lamports/grand_total_sol = total fees collected; total_trade_volume_lamports/sol = summed trade notional; effective_fee_rate_bps = grand_total/volume*10000 (0 if no volume); by_side = array of {side (trade_side: buy/sell), total_lamports, count}. > Read-scope, tenant-scoped. All fee rows are filtered to users of the calling app. ### GET /v1/fees/by-source **Fees by source** — Returns the calling app's fee revenue broken down by trade source (manual, dca, limit, copy_trade, tg_auto, x_auto, afk, etc.). Scoped to ctx.app_id; the legacy app_id query param is accepted but ignored. `GET https://api.stryke.gg/v1/fees/by-source` · auth: `Authorization: Bearer` **Query parameters:** - `app_id` (int, optional) — Accepted for backward compat but IGNORED — scope is always the caller's own app_id. **Response:** data.sources = array of {source (trade_source), total_lamports/total_sol (fees), total_trade_lamports/total_trade_sol (trade notional), count}, ordered by total_lamports DESC. > Read-scope, tenant-scoped. The ?app_id= param is ignored to prevent cross-tenant fee enumeration. ### GET /v1/fees/hourly **Fees hourly** — Returns the calling app's fee collection bucketed by hour over a lookback window. Scoped per-tenant via api_app_users.app_id. `GET https://api.stryke.gg/v1/fees/hourly` · auth: `Authorization: Bearer` **Query parameters:** - `hours` (int, optional) — Hours to look back (default 24, must be 1–720). **Response:** data.hours = echoed lookback window; data.data = array of hourly buckets {hour (RFC3339 hour-truncated), total_lamports (fees), count (trades)}, ordered by hour DESC. > Read-scope, tenant-scoped. ### GET /v1/fees/rates **Fee rates** — Returns the current platform fee BPS rates per trade source and the treasury wallet, read from server environment config. Not tenant-specific (global server config). `GET https://api.stryke.gg/v1/fees/rates` · auth: `Authorization: Bearer` **Response:** data fields are per-source fee BPS from env (MANUAL_FEE_BPS/DCA_FEE_BPS/LIMIT_FEE_BPS/COPY_TRADE_FEE_BPS/a platform setting/a platform setting default 100; AFK_FEE_BPS default 0) plus treasury = a platform setting pubkey string. > Read-scope. Values come from server.env, not per-tenant DB; no DB access so no 503 path. ### GET /v1/pnl/positions **List open positions (PNL)** — Returns all currently open positions for the caller's active wallet, each aggregated from its success trades (total buy/sell amounts, buy/sell counts, average entry price in USD and SOL, total tips). Tenant-scoped: resolves user_id from X-User-Ref to the active wallet_id; returns an empty list if the user has no wallet yet. `GET https://api.stryke.gg/v1/pnl/positions` · auth: `Authorization: Bearer` **Response:** positions[] aggregates trades joined on positions where status='open'. total_buys_lamports = sum of buy input_amount; total_sells_lamports = sum of sell output_amount; avg_entry_price_usd/sol are input-amount-weighted buy prices; first_trade_at is position opened_at, last_trade_at is max trade created_at; source is the position open source (swap/copy_trade/sniper/dca/limit). count = length of positions[]. > Returns {"success":true,"count":0,"positions":[]} (HTTP 200) when the user has no active wallet. Requires X-User-Ref tenancy header. ### GET /v1/pnl/position **Open position detail (PNL)** — Returns the single open position for a given mint on the caller's active wallet, with aggregated trade stats (total buys/sells in lamports, buy/sell counts) plus position metadata. Tenant-scoped via X-User-Ref → active wallet_id. `GET https://api.stryke.gg/v1/pnl/position` · auth: `Authorization: Bearer` **Query parameters:** - `mint` (string, required) — Token mint address (required, non-empty). **Response:** position is null (with success:true) when no open position exists for the mint or the user has no wallet. total_buys_lamports/total_sells_lamports and buy_count/sell_count are computed from get_trades_by_token; remaining fields come from the positions row. > No active wallet returns {"success":true,"position":null} (HTTP 200). Requires X-User-Ref tenancy header. ### GET /v1/pnl/history **Closed position history (realized PNL)** — Returns closed positions for the caller's active wallet with realized PNL (SOL lamports + USD, percent returns, costs, proceeds, fees, tips), most recent first. Tenant-scoped via X-User-Ref → active wallet_id. `GET https://api.stryke.gg/v1/pnl/history` · auth: `Authorization: Bearer` **Query parameters:** - `limit` (int, optional) — Max results, default 50, clamped to 1..200. **Response:** positions[] are ClosedPositionSummary rows. realized_pnl_sol_lamports is realized SOL PnL in lamports; realized_pnl_usd is USD PnL; pnl_sol_percent/pnl_usd_percent are percent returns; total_cost/proceeds/platform_fee/tip are lamports; close_sol_price_usd is the SOL/USD price at close. count = length of positions[]. Ordered by closed_at descending (DB helper). > No active wallet returns {"success":true,"count":0,"positions":[]} (HTTP 200). limit is clamped to max 200 / min 1. Requires X-User-Ref tenancy header. ### GET /v1/positions/list **List open positions** — Returns all open positions for the authenticated user's active wallet, aggregated from the positions+trades join (total buys/sells in lamports, buy/sell counts, average entry price in USD and SOL, total tips). Tenant-scoped: resolves user_id from the request context and the wallet via the active wallet; if no wallet is found it returns an empty list rather than an error. `GET https://api.stryke.gg/v1/positions/list` · auth: `Authorization: Bearer` **Response:** success=true, count=number of open positions, positions[] each with token_mint, token_name, token_symbol, dex_type, total_buys_lamports / total_sells_lamports (summed input/output amounts), buy_count/sell_count, avg_entry_price_usd / avg_entry_price_sol (buy-volume-weighted), first_trade_at (position opened_at, unix secs), last_trade_at, source (swap/copy_trade/sniper/dca/limit), total_tips_lamports. Only trades with status='success' contribute to the aggregates. ### GET /v1/positions/{id}/trades **List trades for a position** — Returns every trade belonging to a specific position id, ordered oldest-first. Ownership is enforced by joining trades to positions and filtering on the caller's resolved wallet_id, so a position id belonging to another tenant returns an empty trades list. If the caller has no active wallet it returns 404. `GET https://api.stryke.gg/v1/positions/{id}/trades` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — Position id (i32) to fetch trades for. Scoped to the caller's wallet via JOIN. **Response:** success=true, position_id echoes the path param, count=number of trades, trades[] each with id, wallet_id, trade_type (buy/sell), token_mint/name/symbol, dex_type, input_amount, output_amount, price_usd, price_sol, signature, status, buy_mode, position_id, created_at (unix secs), tip_lamports, platform_fee_lamports, sol_price_usd. Ordered created_at ASC. ### POST /v1/positions/pnl-card **(Not yet available) Generate PNL card** — Intended to render a PNL card PNG, but requires a separate image-rendering service not available in DB-only mode, so it always returns 501. `POST https://api.stryke.gg/v1/positions/pnl-card` · auth: `Authorization: Bearer` **Body parameters:** - `body` (object, optional) — Arbitrary JSON body (accepted but ignored; the handler returns 501 before reading it). **Response:** Always 501 NOT_IMPLEMENTED. Body is accepted via ApiJson but never processed. > 501 stub (not yet implemented) — requires a separate PNG rendering service (P3+). ### GET /v1/positions/trades **List trades by token mint** — Returns all trades for a given token mint across the caller's active wallet (via db.get_trades_by_token). The ?mint= query parameter is required. If the caller has no active wallet it returns an empty trades list rather than an error. `GET https://api.stryke.gg/v1/positions/trades` · auth: `Authorization: Bearer` **Query parameters:** - `mint` (string, required) — Token mint address to filter trades by. Required and must be non-empty. **Response:** success=true, mint echoes the query param, count=number of trades, trades[] same per-trade shape as positions/{id}/trades (id, wallet_id, trade_type, token_mint/name/symbol, dex_type, input_amount, output_amount, price_usd, price_sol, signature, status, buy_mode, position_id, created_at, tip_lamports, platform_fee_lamports, sol_price_usd). ## Trading API: Indexer Auth: send your key as `Authorization: Bearer ` (the trading API uses Bearer, NOT the X-API-Key header used by the data API). Read-only on-chain token + pool index — metadata, pool params, recent tokens/pools, search, and per-DEX stats. ### GET /v1/indexer/token **Get indexed token metadata** — Looks up a single token in the in-process indexer by mint and returns its on-chain metadata (name/symbol/decimals/token_program/uri/supply/creator/source). Read-only the database query against the indexer's 460K+ token cache. `GET https://api.stryke.gg/v1/indexer/token` · auth: `Authorization: Bearer` **Query parameters:** - `mint` (string, required) — Token mint address to look up **Response:** data is the full token row, or null when the mint isn't indexed. success:true even on a miss (data:null). > Errors are returned as success:false envelopes in a 200 body (not HTTP status codes): missing mint -> code=indexer_error; no indexer -> code=indexer_unavailable. ### GET /v1/indexer/pool **Get indexed pool by mint** — Returns the latest indexed pool for a mint, or a specific DEX's pool when dex_type is supplied (synonyms normalized). Carries pool_address, vaults, token programs, raw dex_data, creator and launch slot/signature/block_time for fast pool-param lookups. `GET https://api.stryke.gg/v1/indexer/pool` · auth: `Authorization: Bearer` **Query parameters:** - `mint` (string, required) — Token mint address - `dex_type` (string, optional) — Optional DEX filter (canonical or synonym, e.g. pumpfun/pump, raydium_cpmm/raycpmm, orca/whirlpool). When omitted, returns the most recently indexed pool for the mint. **Response:** data is a full pool row or null when no pool is indexed for the mint. dex_data is a JSON string of DEX-specific layout fields. > Error conditions are returned as success:false envelopes inside a 200 body, not via HTTP status codes. ### GET /v1/indexer/pool-by-address **Get indexed pool by address** — Looks up a single indexed pool directly by its on-chain pool/pair address and returns the same pool row shape as /indexer/pool. `GET https://api.stryke.gg/v1/indexer/pool-by-address` · auth: `Authorization: Bearer` **Query parameters:** - `address` (string, required) — Pool/pair on-chain address **Response:** data is the pool row or null when the address isn't indexed. > Error conditions are returned as success:false envelopes inside a 200 body. ### GET /v1/indexer/recent-tokens **List recent indexed tokens** — Returns the most recently indexed tokens, optionally filtered to one DEX. Sorted by indexed_at DESC. Returns lightweight token metadata rows (no price/metrics). `GET https://api.stryke.gg/v1/indexer/recent-tokens` · auth: `Authorization: Bearer` **Query parameters:** - `limit` (int, optional) — Max rows, default 20, clamped to 1..200 - `dex_type` (string, optional) — Optional DEX filter (canonical or synonym). When omitted, returns recent tokens across all DEXes. **Response:** data is an array of token rows (TokenResponse). Empty array when none match. > DB errors return success:false code=indexer_error inside a 200 body. limit is clamped server-side to 1..200. ### GET /v1/indexer/recent-by-dex **Recent tokens grouped by DEX (metrics-enriched)** — Returns the most recent tokens for every supported DEX in one call, grouped by DEX name, each enriched with token_metrics (price/mcap/liquidity/rolling-window volume + swap counts) and resolved off-chain metadata (image/socials/description). USD values computed server-side from the cached SOL/USD price. `GET https://api.stryke.gg/v1/indexer/recent-by-dex` · auth: `Authorization: Bearer` **Query parameters:** - `limit` (int, optional) — Max rows per DEX, default 5, clamped to 1..50 **Response:** data is a map: sol_usd (the SOL/USD price used for conversions) plus one key per DEX (only DEXes with rows are included) whose value is an array of metrics-enriched token rows. price_sol falls back to creation reserves when no live swap price exists; mcap derived from price x circulating supply when the aggregator value is absent. > Per-DEX queries run concurrently (one tokio task per DEX in dex_registry::ALL_DB_NAMES). DEXes with no rows are omitted from the map. ### GET /v1/indexer/recent-pools **List recent pool launches** — Pool launches sorted by indexer insertion time (DESC), LEFT JOIN'd with indexed_tokens so name/symbol/uri are included once enrichment lands. Each row has a launch tx signature to deep-link to Solscan. Optional DEX filter validated against the canonical allow-list. `GET https://api.stryke.gg/v1/indexer/recent-pools` · auth: `Authorization: Bearer` **Query parameters:** - `dex_type` (string, optional) — Optional DEX filter (canonical or synonym; validated against the allow-list). When omitted, returns launches across all DEXes. - `limit` (int, optional) — Max rows (default 50; not clamped here, passed to the query) **Response:** data is an array of RecentPoolResponse. name/symbol/uri are nullable (LEFT JOIN — null until token enrichment catches up). block_time may be null on the first batch after a fresh restart; indexed_at is always populated. > Unknown dex_type is rejected up front (validated against dex_registry::ALL_DB_NAMES). Error conditions returned as success:false envelopes in a 200 body. ### GET /v1/indexer/search **Search tokens** — Case-insensitive LIKE search over indexed tokens by name/symbol/mint, returning DB-metric-enriched results (price_sol, volume_24h_usd, liquidity_usd, mcap_usd) sorted by the chosen key. USD values computed from the cached SOL/USD price. `GET https://api.stryke.gg/v1/indexer/search` · auth: `Authorization: Bearer` **Query parameters:** - `q` (string, required) — Search query (alias: query). Required and non-empty. - `sort` (string, optional) — Sort key, default 'mc' (market cap) - `limit` (int, optional) — Max rows, default 10, capped at 50 **Response:** Includes a top-level count plus data array. Metrics are DB-only (no live SwapAggregator enrichment in this state); callers needing rolling-window stats should use /indexer/stats or the test-api. LIKE special chars in q are escaped server-side. > q accepts the alias 'query'. Error conditions returned as success:false envelopes in a 200 body. ### GET /v1/indexer/stats **Indexer statistics** — Returns aggregate indexer counters: total/enriched/pending/failed pools, total tokens, and a per-DEX pool-count breakdown. `GET https://api.stryke.gg/v1/indexer/stats` · auth: `Authorization: Bearer` **Response:** data carries the four pool-state counters, total_tokens, and pools_per_dex (a map of canonical DEX name to pool count). > DB errors return success:false code=indexer_error in a 200 body. pools_per_dex defaults to empty on its sub-query failing. ### POST /v1/indexer/tokens/batch **Batch token lookup** — Enriches up to 100 mints in one round-trip, returning a map of mint to its full token row. Mints not found are omitted from the map (not null-valued) so callers can detect coverage gaps cheaply. `POST https://api.stryke.gg/v1/indexer/tokens/batch` · auth: `Authorization: Bearer` **Body parameters:** - `mints` (string[], required) — Token mint addresses to look up (max 100). Empty array returns an empty map. **Response:** data is a map keyed by mint to the full token row (TokenResponse). Mints not indexed are absent from the map. Empty mints array yields data:{}. > Body parsed via ApiJson (returns a structured error envelope on malformed JSON). The >100-mints and no-indexer cases return success:false envelopes in a 200 body. ## Trading API: Limit Orders & DCA Auth: send your key as `Authorization: Bearer ` (the trading API uses Bearer, NOT the X-API-Key header used by the data API). Automated limit orders and dollar-cost-averaging schedules. automation scope; per-tenant; supports an idempotency key. ### POST /v1/limit/create **Create limit order** — Creates a limit/conditional order row in the limit_orders table for the calling tenant's active wallet (price/schedule/event trigger, buy or sell). DB-only write; no on-chain action — execution is handled later by the bot's PriceMonitor service. Scoped to the user resolved from the API key + X-User-Ref. `POST https://api.stryke.gg/v1/limit/create` · auth: `Authorization: Bearer` **Body parameters:** - `mint` (string, required) — Token mint address. - `amount_sol` (number, required) — Order size in SOL (converted to lamports). Required — 422 if omitted. - `trigger_type` (string, optional) — One of price | schedule | event. Default 'price'. - `trigger_price_usd` (number, optional) — Target USD price for price triggers (stored as 0.0 if omitted). - `trigger_time_seconds` (int, optional) — For trigger_type=schedule: seconds from now until trigger_at. - `event_type` (string, optional) — For trigger_type=event: event name (e.g. 'migration'). Stored as trigger_direction 'event:'. Defaults to 'migration'. - `slippage_bps` (int, optional) — Slippage tolerance in bps. Default 1000. - `mev_protect` (bool, optional) — Route via MEV-protected providers. Default true. - `expiry_seconds` (int, optional) — Seconds until the order expires. Default 86400 (24h). - `side` (string, optional) — buy | sell. Default 'buy'. Determines trigger_direction (sell=below, buy=above) for price triggers. - `sell_percent` (int, optional) — For sell orders: percent of holdings to sell. - `trailing_pct` (number, optional) — Trailing stop percentage (optional). - `max_retries` (int, optional) — Max execution retries. Default 0. **Response:** success=true with the full created order object (from order_to_json). order.id is the new order id; status starts 'pending'; amount_sol is derived from amount_lamports. If the row can't be re-read, order collapses to just {"id":}. > Honors Idempotency-Key header (automation routes pass through the idempotency layer). ### GET /v1/limit/list **List pending limit orders** — Returns all pending limit orders for the calling tenant's user. Read from the limit_orders table, scoped to the resolved user id. `GET https://api.stryke.gg/v1/limit/list` · auth: `Authorization: Bearer` **Response:** count = number of pending orders; orders is an array of the same order object shape as /limit/create. Only status='pending' orders are returned. ### GET /v1/limit/monitor **(Not yet available) Limit monitor status** — Intended to report PriceMonitor execution status. Not yet wired in the multi-tenant API — returns 501. `GET https://api.stryke.gg/v1/limit/monitor` · auth: `Authorization: Bearer` **Response:** Always 501 — the price-monitor/trading-executor service is not available in this API process. > 501 stub (not yet implemented). ### DELETE /v1/limit/all **Cancel all limit orders** — Cancels every pending limit order for the calling tenant's user. Scoped to the resolved user id. `DELETE https://api.stryke.gg/v1/limit/all` · auth: `Authorization: Bearer` **Response:** cancelled = number of orders that were cancelled. > Honors Idempotency-Key header (automation routes pass through the idempotency layer). ### GET /v1/limit/{id} **Get limit order** — Fetches a single limit order by id, scoped to the calling tenant's user (cross-user access blocked by user_id filter). `GET https://api.stryke.gg/v1/limit/{id}` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — Limit order id (i64). **Response:** success=true with the full order object (order_to_json). If not found/owned, success=false with error 'Order not found' (HTTP 200). ### DELETE /v1/limit/{id} **Cancel limit order** — Cancels a single pending limit order by id, scoped to the calling tenant's user. Idempotent per order (cancelling an already-cancelled/unknown order returns success=false). `DELETE https://api.stryke.gg/v1/limit/{id}` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — Limit order id (i64). **Response:** success=true with a confirmation message when cancelled. If the order isn't found/owned or was already cancelled, success=false with error 'Order not found or already cancelled' (HTTP 200). > Honors Idempotency-Key header (automation routes pass through the idempotency layer). ### POST /v1/dca/create **(Not yet available) Create DCA order** — Intended to create a recurring dollar-cost-average buy/sell order for the authenticated tenant. Currently a 501 stub: it requires rpc/indexer resources that are not yet wired into V1State (P1 Group C), so it always returns NOT_IMPLEMENTED. `POST https://api.stryke.gg/v1/dca/create` · auth: `Authorization: Bearer` **Body parameters:** - `mint` (string, required) — Token mint address to DCA into/out of. - `amount_sol` (number, optional) — SOL amount per interval (buy side). - `interval_seconds` (int, required) — Seconds between each DCA execution. - `total_orders` (int, optional) — Total number of orders to execute before the schedule completes. - `min_price` (number, optional) — Lower price guard (USD); skip execution below this. - `max_price` (number, optional) — Upper price guard (USD); skip execution above this. - `slippage_bps` (int, optional) — Slippage tolerance in basis points. Default 1000. - `mev_protect` (bool, optional) — Route via MEV-protected providers. Default true. - `duration_seconds` (int, optional) — Overall lifetime of the schedule in seconds before it expires. - `side` (string, optional) — DCA direction: 'buy' or 'sell'. Default 'buy'. - `sell_percent` (int, optional) — Percent of holdings to sell each interval (sell side). **Response:** Always returns 501 NOT_IMPLEMENTED. Once wired it will create a dca_orders row scoped to the authenticated tenant. > 501 stub (not yet implemented). Mutation route — supports Idempotency-Key header via the automation idempotency layer. ### GET /v1/dca/list **List DCA orders** — Returns all active and paused DCA orders for the authenticated tenant (resolved from X-User-Ref via the tenancy layer). Active orders are listed first, then paused. `GET https://api.stryke.gg/v1/dca/list` · auth: `Authorization: Bearer` **Response:** count = number of orders returned (active + paused combined). Each order: amount_lamports/amount_sol = per-interval size; orders_executed/total_orders = progress; total_sol_spent (lamports) and total_sol_spent_display (SOL) = cumulative spend; total_tokens_bought = cumulative tokens acquired; status is 'active' or 'paused'; timestamps are RFC3339 (nullable). ### GET /v1/dca/{id} **Get DCA order** — Returns a single DCA order by id, scoped to the authenticated tenant (dca_get_by_id filters by user). Returns success:false with 'Order not found' if it does not exist or belongs to another tenant. `GET https://api.stryke.gg/v1/dca/{id}` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — DCA order id (i64). **Response:** order is the full order_to_json shape (same fields as /v1/dca/list elements). On miss returns {"success":false,"error":"Order not found"} with HTTP 200; on DB failure {"success":false,"error":"DB error:..."}. > Not-found and DB errors are returned as success:false in a 200 body, not as HTTP error codes. ### POST /v1/dca/{id}/pause **Pause DCA order** — Pauses an active DCA order owned by the authenticated tenant (dca_pause filters by user). No further executions occur until resumed. `POST https://api.stryke.gg/v1/dca/{id}/pause` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — DCA order id (i64). **Response:** success:true with a message when the order transitions active→paused. {"success":false,"error":"Order not found or not active"} if no matching active order; {"success":false,"error":"Failed to pause:..."} on DB error. > Mutation route — supports Idempotency-Key header via the automation idempotency layer. Not-found/DB errors returned as success:false in 200 body. ### POST /v1/dca/{id}/resume **Resume DCA order** — Resumes a paused DCA order owned by the authenticated tenant (dca_resume filters by user). Re-enables scheduled executions. `POST https://api.stryke.gg/v1/dca/{id}/resume` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — DCA order id (i64). **Response:** success:true with a message when the order transitions paused→active. {"success":false,"error":"Order not found or not paused"} if no matching paused order; {"success":false,"error":"Failed to resume:..."} on DB error. > Mutation route — supports Idempotency-Key header via the automation idempotency layer. Not-found/DB errors returned as success:false in 200 body. ### DELETE /v1/dca/{id} **Cancel DCA order** — Cancels a single DCA order owned by the authenticated tenant (dca_cancel filters by user). Idempotent at the data level — already-cancelled orders return not-found. `DELETE https://api.stryke.gg/v1/dca/{id}` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — DCA order id (i64). **Response:** success:true with a message when an active/paused order is cancelled. {"success":false,"error":"Order not found or already cancelled"} if no matching order; {"success":false,"error":"Failed to cancel:..."} on DB error. > Mutation route — supports Idempotency-Key header via the automation idempotency layer. Not-found/DB errors returned as success:false in 200 body. ### DELETE /v1/dca/all **Cancel all DCA orders** — Cancels every DCA order for the authenticated tenant (dca_cancel_all scoped by user) and returns how many were cancelled. `DELETE https://api.stryke.gg/v1/dca/all` · auth: `Authorization: Bearer` **Response:** cancelled = number of orders cancelled for this tenant (0 if none). {"success":false,"error":"Failed to cancel:..."} on DB error. > Mutation route — supports Idempotency-Key header via the automation idempotency layer. Registered before /dca/{id} so 'all' is not captured as an id path param. ## Trading API: Copy Trade Auth: send your key as `Authorization: Bearer ` (the trading API uses Bearer, NOT the X-API-Key header used by the data API). Copy-trading configs, sources, filters, blacklists, and execution stats. automation scope; per-tenant. ### POST /v1/copy-trade/start **(Not yet available) Start copy-trade service** — Intended to start the live copy-trading engine for the caller. Currently a 501 stub — the copy-trade runtime service is not wired into V1State in this deployment. `POST https://api.stryke.gg/v1/copy-trade/start` · auth: `Authorization: Bearer` **Response:** Always returns the not_implemented envelope. Calls require_user first, so X-User-Ref is still required. > 501 stub (not yet implemented). Requires X-User-Ref. Idempotency-Key header accepted (automation idempotency layer). ### POST /v1/copy-trade/stop **(Not yet available) Stop copy-trade service** — Intended to stop the live copy-trading engine for the caller. Currently a 501 stub — the copy-trade runtime service is not wired into V1State. `POST https://api.stryke.gg/v1/copy-trade/stop` · auth: `Authorization: Bearer` **Response:** Always returns the not_implemented envelope. Calls require_user first, so X-User-Ref is still required. > 501 stub (not yet implemented). Requires X-User-Ref. Idempotency-Key header accepted. ### GET /v1/copy-trade/status **(Not yet available) Copy-trade service status (caller)** — Intended to report the caller's copy-trade engine status. Currently a 501 stub — requires the live copy-trade service not wired into V1State. `GET https://api.stryke.gg/v1/copy-trade/status` · auth: `Authorization: Bearer` **Response:** Always returns the not_implemented envelope. Calls require_user first, so X-User-Ref is still required. > 501 stub (not yet implemented). Requires X-User-Ref. ### GET /v1/copy-trade/system-status **(Not yet available) Copy-trade system status** — Intended to report global copy-trade engine/system health. Currently a 501 stub — requires the live copy-trade service not wired into V1State. `GET https://api.stryke.gg/v1/copy-trade/system-status` · auth: `Authorization: Bearer` **Response:** Always returns the not_implemented envelope. Calls require_user first, so X-User-Ref is still required. > 501 stub (not yet implemented). Requires X-User-Ref. No utoipa annotation (undocumented in OpenAPI). ### POST /v1/copy-trade/add-source **(Not yet available) Add copy-trade source wallet** — Intended to register a source wallet to follow. Currently a 501 stub — requires the live copy-trade service not wired into V1State; the body is accepted but ignored. `POST https://api.stryke.gg/v1/copy-trade/add-source` · auth: `Authorization: Bearer` **Body parameters:** - `source_id` (int, required) — Source id to attach. - `wallet_address` (string, required) — Solana wallet address of the source to follow. **Response:** Always returns the not_implemented envelope; body is parsed (ApiJson) but never used. > 501 stub (not yet implemented). Idempotency-Key header accepted. No utoipa annotation. ### POST /v1/copy-trade/remove-source **(Not yet available) Remove copy-trade source wallet** — Intended to unregister a source wallet. Currently a 501 stub — requires the live copy-trade service not wired into V1State; the body is accepted but ignored. `POST https://api.stryke.gg/v1/copy-trade/remove-source` · auth: `Authorization: Bearer` **Body parameters:** - `source_id` (int, required) — Source id to remove. **Response:** Always returns the not_implemented envelope; body is parsed (ApiJson) but never used. > 501 stub (not yet implemented). Idempotency-Key header accepted. No utoipa annotation. ### POST /v1/copy-trade/create-config **Create copy-trade config** — Creates a new copy-trade config for the caller's active wallet following the given source wallet, then activates it. Scoped to the X-User-Ref tenant; uses BuyMode::Fixed with buy_amount_sol. `POST https://api.stryke.gg/v1/copy-trade/create-config` · auth: `Authorization: Bearer` **Body parameters:** - `source_wallet` (string, required) — Solana pubkey of the wallet to copy. - `buy_amount_sol` (number, required) — Fixed SOL amount per copied buy. Must be between 0.001 and 100. - `buy_slippage_bps` (int, optional) — Buy slippage in bps. Defaults to 1000 (10%). - `sell_slippage_bps` (int, optional) — Sell slippage in bps. Defaults to 1000 (10%). - `name` (string, optional) — Config label. Defaults to 'API Config'. - `observe_only` (bool, optional) — If true, config observes/logs without executing trades. Defaults to false. **Response:** Returns the new config_id and the resolved source_id. Config is created, activated (ct_toggle_config true), optional observe_only set, and the source row get-or-created. > Requires X-User-Ref (resolves bot user + active wallet). Idempotency-Key header accepted. Validation/business failures return HTTP 200 with {success:false,error:"..."}. ### POST /v1/copy-trade/delete-config **Delete copy-trade config** — Deletes one of the caller's copy-trade configs by id. Ownership-scoped to the X-User-Ref tenant (ct_delete_config filters by user). `POST https://api.stryke.gg/v1/copy-trade/delete-config` · auth: `Authorization: Bearer` **Body parameters:** - `config_id` (int, required) — Id of the config to delete. **Response:** Returns a confirmation message on success. > Requires X-User-Ref. Idempotency-Key header accepted. No utoipa annotation. ### GET /v1/copy-trade/configs **List copy-trade configs** — Lists all copy-trade configs owned by the caller (X-User-Ref tenant), each fully serialized via config_to_json. `GET https://api.stryke.gg/v1/copy-trade/configs` · auth: `Authorization: Bearer` **Response:** configs[] is the full config object (config_to_json); count is the array length. Empty array if the user has no configs. > Requires X-User-Ref. ### GET /v1/copy-trade/config/{id} **Get copy-trade config** — Returns one copy-trade config by id, fully serialized. Ownership-scoped to the X-User-Ref tenant (ct_get_config filters by user). `GET https://api.stryke.gg/v1/copy-trade/config/{id}` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — Config id. **Response:** config is the full config_to_json object (same field set as list_configs entries). > Requires X-User-Ref. Not-found returns HTTP 200 with success:false ("Config {id} not found"). ### PUT /v1/copy-trade/config/{id} **Update copy-trade config** — Partial update of a copy-trade config — every body field is optional and only provided fields are applied. Ownership-scoped; returns the full updated config. `PUT https://api.stryke.gg/v1/copy-trade/config/{id}` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — Config id. **Body parameters:** - `buy_mode` (string, optional) — One of: fixed, exact, percent. - `buy_amount_sol` (number, optional) — Fixed buy amount in SOL (converted to lamports). - `buy_percent` (int, optional) — Percent buy size (used with buy_mode=percent). - `max_buy_amount_sol` (number, optional) — Cap per buy in SOL. - `min_trigger_buy_sol` (number, optional) — Min source-buy size (SOL) to trigger a copy. - `max_trigger_buy_sol` (number, optional) — Max source-buy size (SOL) to trigger a copy. - `buy_slippage_bps` (int, optional) — Buy slippage in bps. - `sell_slippage_bps` (int, optional) — Sell slippage in bps. - `buy_priority_fee` (int, optional) — Buy priority fee (lamports). - `sell_priority_fee` (int, optional) — Sell priority fee (lamports). - `buy_tip` (int, optional) — Buy MEV tip (lamports). - `sell_tip` (int, optional) — Sell MEV tip (lamports). - `copy_buys` (bool, optional) — Copy source buys. - `copy_sells` (bool, optional) — Copy source sells. - `follow_sell_percent` (bool, optional) — Mirror source sell percentages. - `buy_protection` (bool, optional) — Enable buy-side protection. - `sell_protection` (bool, optional) — Enable sell-side protection. - `first_interaction_only` (bool, optional) — Only copy first interaction with a token. - `sell_only_copied` (bool, optional) — Only sell tokens that were copy-bought. - `buy_only_once` (bool, optional) — Buy each token at most once. - `skip_deploys` (bool, optional) — Skip token deploy/creation events. - `reverse_mode` (bool, optional) — Reverse-copy (sell when source buys, etc.). - `sell_on_transfer` (bool, optional) — Sell when source transfers out. - `auto_tip` (bool, optional) — Auto-compute MEV tip. - `observe_only` (bool, optional) — Observe/log without executing. - `reverse_min_sell_percent` (int, optional) — Min sell percent for reverse mode (u8). - `start_time` (int, optional) — Active window start (unix seconds). - `end_time` (int, optional) — Active window end (unix seconds). - `max_buy_count` (int, optional) — Lifetime cap on buys. - `max_per_token` (int, optional) — Max buys per token. - `name` (string, optional) — Config label. - `notify_success` (bool, optional) — Notify on successful copy. - `notify_failed` (bool, optional) — Notify on failed copy. - `notify_skipped` (bool, optional) — Notify on skipped copy. - `notify_filtered` (bool, optional) — Notify on filtered copy. - `autosell_profile_id` (int, optional) — Attach an autosell profile id; <=0 clears it. **Response:** Re-reads and returns the full updated config (config_to_json) after applying all provided fields. Fields are written via several typed DB setters; first failing setter aborts with an error. > Requires X-User-Ref. Idempotency-Key header accepted. Invalid buy_mode / not-found / DB errors return HTTP 200 with success:false. ### POST /v1/copy-trade/config/{id}/toggle **Toggle copy-trade config active** — Activates or deactivates a config. Body must contain the boolean `active`. Ownership-scoped to the X-User-Ref tenant. `POST https://api.stryke.gg/v1/copy-trade/config/{id}/toggle` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — Config id. **Body parameters:** - `active` (bool, required) — true to activate, false to deactivate. **Response:** Message is 'Config {id} activated' or 'Config {id} deactivated'. Body is a free-form JSON object; only `active` is read. > Requires X-User-Ref. Idempotency-Key header accepted. Missing `active` returns HTTP 200 with success:false. ### POST /v1/copy-trade/config/{id}/reset-count **Reset copy-trade buy count** — Resets the lifetime buy counter (current_buy_count) for a config. Ownership-scoped to the X-User-Ref tenant. `POST https://api.stryke.gg/v1/copy-trade/config/{id}/reset-count` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — Config id. **Response:** Confirmation message on success. > Requires X-User-Ref. Idempotency-Key header accepted. No request body. ### GET /v1/copy-trade/config/{id}/blacklist **Get config blacklist** — Returns the token-mint blacklist for a config. Ownership-scoped to the X-User-Ref tenant. `GET https://api.stryke.gg/v1/copy-trade/config/{id}/blacklist` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — Config id. **Response:** blacklist is an array of token-mint strings; count is its length. > Requires X-User-Ref. ### POST /v1/copy-trade/config/{id}/blacklist **Add mint to config blacklist** — Adds a token mint to the config's blacklist (validates the pubkey first). Ownership-scoped to the X-User-Ref tenant. `POST https://api.stryke.gg/v1/copy-trade/config/{id}/blacklist` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — Config id. **Body parameters:** - `token_mint` (string, required) — Token mint pubkey to blacklist. **Response:** Confirmation message echoing the added mint. > Requires X-User-Ref. Idempotency-Key header accepted. Invalid mint returns HTTP 200 with success:false. ### DELETE /v1/copy-trade/config/{id}/blacklist **Remove mint from config blacklist** — Removes a token mint from the config's blacklist. Ownership-scoped to the X-User-Ref tenant. `DELETE https://api.stryke.gg/v1/copy-trade/config/{id}/blacklist` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — Config id. **Body parameters:** - `token_mint` (string, required) — Token mint pubkey to remove. **Response:** Confirmation message echoing the removed mint. > Requires X-User-Ref. Idempotency-Key header accepted. Body (token_mint) required even though this is DELETE. ### POST /v1/copy-trade/config/{id}/blacklist/clear **Clear config blacklist** — Removes all entries from the config's blacklist. Ownership-scoped to the X-User-Ref tenant. `POST https://api.stryke.gg/v1/copy-trade/config/{id}/blacklist/clear` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — Config id. **Response:** Confirmation message on success. > Requires X-User-Ref. Idempotency-Key header accepted. No request body. ### GET /v1/copy-trade/config/{id}/filters **Get config filters** — Returns the token filter set (mcap, token_age, liquidity ranges + platform allow-list) for a config. Ownership-scoped to the X-User-Ref tenant. `GET https://api.stryke.gg/v1/copy-trade/config/{id}/filters` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — Config id. **Response:** mcap/token_age/liquidity are {min,max} range objects (or null); platforms is a string array; has_filters is true if any filter is set. > Requires X-User-Ref. ### PUT /v1/copy-trade/config/{id}/filters **Update config filters** — Replaces the config's filter set from min/max bounds for mcap, token age, and liquidity plus a platform allow-list. A range is set only if its min or max is provided. Ownership-scoped to the X-User-Ref tenant. `PUT https://api.stryke.gg/v1/copy-trade/config/{id}/filters` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — Config id. **Body parameters:** - `mcap_min` (int, optional) — Min market cap. - `mcap_max` (int, optional) — Max market cap. - `age_min` (int, optional) — Min token age. - `age_max` (int, optional) — Max token age. - `liquidity_min` (int, optional) — Min liquidity. - `liquidity_max` (int, optional) — Max liquidity. - `platforms` (string[], optional) — Allowed platforms/DEXes. **Response:** Echoes the persisted filter JSON. The full filter set is overwritten by the request (omitting a range clears it). > Requires X-User-Ref. Idempotency-Key header accepted. ### GET /v1/copy-trade/config/{id}/executions **List config executions** — Returns the copy-trade execution history for a config (source vs. our trade legs, signatures, latency, status). Ownership-scoped to the X-User-Ref tenant. `GET https://api.stryke.gg/v1/copy-trade/config/{id}/executions` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — Config id. **Query parameters:** - `limit` (int, optional) — Max rows (default 50, capped at 200). - `offset` (int, optional) — Row offset for pagination (default 0). **Response:** executions[] holds per-trade audit rows; count is the page length; limit/offset echo the applied paging (limit clamped to ≤200). > Requires X-User-Ref. ### GET /v1/copy-trade/config/{id}/stats **Get config stats** — Returns aggregate execution stats for one config (totals by outcome, success rate, avg latency). Ownership-scoped to the X-User-Ref tenant. `GET https://api.stryke.gg/v1/copy-trade/config/{id}/stats` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — Config id. **Response:** stats has total_executions, successful, failed, skipped, filtered counts plus computed success_rate and avg_latency_ms. > Requires X-User-Ref. ### GET /v1/copy-trade/config/{id}/metrics **(Not yet available) Get config live metrics** — Intended to return live runtime metrics for a config. Currently a 501 stub — requires the live copy-trade service not wired into V1State. `GET https://api.stryke.gg/v1/copy-trade/config/{id}/metrics` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — Config id. **Response:** Always returns the not_implemented envelope. Calls require_user first, so X-User-Ref is still required. > 501 stub (not yet implemented). Requires X-User-Ref. ### GET /v1/copy-trade/user-stats **Get user copy-trade stats** — Returns the caller's aggregate copy-trade stats across all their configs (config counts + execution outcome totals + success rate). Scoped to the X-User-Ref tenant. `GET https://api.stryke.gg/v1/copy-trade/user-stats` · auth: `Authorization: Bearer` **Response:** stats aggregates total/active configs and per-outcome execution counts plus computed success_rate across all of the user's configs. > Requires X-User-Ref. ### GET /v1/copy-trade/detection-stats **(Not yet available) Get copy-trade detection stats** — Intended to return source-wallet detection metrics from the live engine. Currently a 501 stub — requires the live copy-trade service not wired into V1State. `GET https://api.stryke.gg/v1/copy-trade/detection-stats` · auth: `Authorization: Bearer` **Response:** Always returns the not_implemented envelope. Calls require_user first, so X-User-Ref is still required. > 501 stub (not yet implemented). Requires X-User-Ref. ## Trading API: AFK Auto-Buy Auth: send your key as `Authorization: Bearer ` (the trading API uses Bearer, NOT the X-API-Key header used by the data API). Hands-off auto-buy configs with mint/deployer whitelists, blacklists, sell stages, and smart-wallet rules. automation scope. ### POST /v1/afk/create-config **Create AFK config** — Creates a new AFK (auto-buy) config for the calling tenant with the given name. The config is created as a draft (is_saved/is_active default off) and scoped to the X-User-Ref tenant. `POST https://api.stryke.gg/v1/afk/create-config` · auth: `Authorization: Bearer` **Body parameters:** - `name` (string, required) — Config display name. Must be non-empty. **Response:** config_id is the new row's integer id. On failure returns HTTP 200 with {"success":false,"error":"..."} (e.g. empty name, DB error). > Requires X-User-Ref header (tenant scoping). Supports Idempotency-Key header (automation idempotency layer). Domain errors (empty name) return HTTP 200 success:false. ### GET /v1/afk/configs **List AFK configs** — Returns all AFK configs owned by the calling tenant (resolved from X-User-Ref). Each config is the full serialized AfkConfig object with ~100 filter fields. `GET https://api.stryke.gg/v1/afk/configs` · auth: `Authorization: Bearer` **Response:** configs[] holds the full config object (see config_to_json: id, name, is_active, is_saved, platforms, buy_amount_sol, slippage_bps, mev_protect, plus ~100 filter columns such as mcap/liquidity/token_age/buy_count/bonding_progress/dev_*/holder/socials/sell-stage thresholds, time_window_start/end, total_buys, total_sells, created_at, updated_at). count = number of configs. > Requires X-User-Ref header (tenant scoping). ### GET /v1/afk/config/{id} **Get AFK config** — Returns a single AFK config by id, scoped to the calling tenant (telegram_id ownership enforced in the DB query). Returns success:false if the config does not belong to the tenant. `GET https://api.stryke.gg/v1/afk/config/{id}` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — AFK config id. **Response:** config is the full AfkConfig object (same shape as list_configs items, ~100 filter fields). Not-found / not-owned returns HTTP 200 {"success":false,"error":"Config not found"}. > Requires X-User-Ref header. Config-not-found returns HTTP 200 success:false (not 404). ### PUT /v1/afk/config/{id} **Update AFK config field** — Updates a single field on an AFK config (one field per call) for the calling tenant. The field name is resolved against the config's column schema (f64/i32/i64/bool/str) plus special handlers for platforms, name, name_exact, name_contains, and time_window_start/end. `PUT https://api.stryke.gg/v1/afk/config/{id}` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — AFK config id. **Body parameters:** - `field` (string, required) — Column/filter name to update (e.g. buy_amount_sol, mcap_min_sol, platforms, name, time_window_start). Unknown names are rejected. - `value` (string, required) — New value as a string; coerced to the column type. "null" or "" clears nullable filters. For platforms: comma-separated list. For time_window_*: HH:MM or HH:MM:SS. **Response:** Echoes field and the coerced value (type depends on the column; platforms echoes an array). Invalid value coercion or unknown field returns HTTP 200 success:false with a descriptive error string. > Requires X-User-Ref header. Supports Idempotency-Key. Validation errors (bad number/bool/time, unknown field) return HTTP 200 success:false. No utoipa annotation but route is live. ### POST /v1/afk/config/{id}/toggle **Toggle AFK config active** — Activates or deactivates an AFK config for the calling tenant. Only configs with is_saved=true can be toggled active (drafts are rejected) — mirrors the UI auto-save-on-activate gate. The engine cache only picks up the flip on next bot restart or UI write. `POST https://api.stryke.gg/v1/afk/config/{id}/toggle` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — AFK config id. **Body parameters:** - `active` (boolean, required) — Desired active state. **Response:** is_active is the new state returned by the UPDATE. If the config is not found, not owned, or not saved, returns HTTP 200 {"success":false,"error":"Config not found or not saved"}. > Requires X-User-Ref header. Supports Idempotency-Key. is_saved=true gate prevents activating drafts. Engine cache not refreshed live (tracked under task #126). ### DELETE /v1/afk/config/{id} **Delete AFK config** — Deletes an AFK config (and its associated lists/stages via DB cascade) for the calling tenant. Ownership enforced by telegram_id in the DB query. `DELETE https://api.stryke.gg/v1/afk/config/{id}` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — AFK config id. **Response:** message confirms deletion. DB error returns HTTP 200 success:false. > Requires X-User-Ref header. Supports Idempotency-Key. ### GET /v1/afk/executions **List AFK executions** — Returns the calling tenant's AFK auto-buy execution log (most recent first), optionally filtered by config_id. Each row records the attempted buy, signature, success flag and market snapshot. `GET https://api.stryke.gg/v1/afk/executions` · auth: `Authorization: Bearer` **Query parameters:** - `limit` (int, optional) — Max rows to return. Default 20, capped at 100. - `config_id` (int, optional) — Filter executions to a single config. **Response:** executions[] fields: id, config_id, mint, dex_type, buy_amount_sol, signature, success, error_message, mcap_at_buy, liquidity_at_buy, executed_at. count = rows returned. > Requires X-User-Ref header. limit is hard-capped server-side at 100. ### POST /v1/afk/simulate **(Not yet available) Simulate AFK filters (stub)** — Intended to evaluate a config's filters against a given mint. Currently not implemented in the multi-tenant API — RPC/indexer are not wired into V1State. `POST https://api.stryke.gg/v1/afk/simulate` · auth: `Authorization: Bearer` **Body parameters:** - `mint` (string, required) — Token mint to simulate against. - `config_id` (int, required) — AFK config id to evaluate. **Response:** Always returns the not_implemented envelope at HTTP 200; never runs a real simulation. > 501 stub (not yet implemented) — returns {success:false, error.code:not_implemented} at HTTP 200, not a real 501 status. Requires X-User-Ref header. ### GET /v1/afk/config/{id}/whitelist-mints **List whitelist mints** — Returns the mint-address whitelist for an AFK config (tenant-scoped). When set, only these mints are eligible for auto-buy. `GET https://api.stryke.gg/v1/afk/config/{id}/whitelist-mints` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — AFK config id. **Response:** mints[] fields: id, address, created_at. count = entries. > Requires X-User-Ref header. ### POST /v1/afk/config/{id}/whitelist-mints **Add whitelist mint** — Adds a mint address to an AFK config's whitelist (tenant-scoped). `POST https://api.stryke.gg/v1/afk/config/{id}/whitelist-mints` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — AFK config id. **Body parameters:** - `address` (string, required) — Mint address to whitelist. Must be non-empty. **Response:** Empty address returns HTTP 200 success:false. > Requires X-User-Ref header. Supports Idempotency-Key. ### DELETE /v1/afk/config/{id}/whitelist-mints **Clear whitelist mints** — Removes all whitelist mint entries from an AFK config (tenant-scoped). `DELETE https://api.stryke.gg/v1/afk/config/{id}/whitelist-mints` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — AFK config id. **Response:** Clears the entire list for the config. > Requires X-User-Ref header. Supports Idempotency-Key. ### GET /v1/afk/config/{id}/whitelist-deployers **List whitelist deployers** — Returns the deployer-address whitelist for an AFK config (tenant-scoped). When set, only tokens from these deployers are eligible. `GET https://api.stryke.gg/v1/afk/config/{id}/whitelist-deployers` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — AFK config id. **Response:** deployers[] fields: id, address, label (nullable), created_at. count = entries. > Requires X-User-Ref header. ### POST /v1/afk/config/{id}/whitelist-deployers **Add whitelist deployer** — Adds a deployer address (with optional label) to an AFK config's deployer whitelist (tenant-scoped). `POST https://api.stryke.gg/v1/afk/config/{id}/whitelist-deployers` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — AFK config id. **Body parameters:** - `address` (string, required) — Deployer wallet address. Must be non-empty. - `label` (string, optional) — Optional human label for the deployer. **Response:** Empty address returns HTTP 200 success:false. > Requires X-User-Ref header. Supports Idempotency-Key. ### DELETE /v1/afk/config/{id}/whitelist-deployers **Clear whitelist deployers** — Removes all deployer whitelist entries from an AFK config (tenant-scoped). `DELETE https://api.stryke.gg/v1/afk/config/{id}/whitelist-deployers` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — AFK config id. **Response:** Clears the entire deployer list for the config. > Requires X-User-Ref header. Supports Idempotency-Key. ### GET /v1/afk/config/{id}/blacklist **Get blacklist (tokens + devs)** — Returns both the token-address and deployer-address blacklists for an AFK config (tenant-scoped) in a single response. `GET https://api.stryke.gg/v1/afk/config/{id}/blacklist` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — AFK config id. **Response:** token_addresses + token_count and dev_addresses + dev_count are returned together (arrays of address strings). > Requires X-User-Ref header. ### POST /v1/afk/config/{id}/blacklist **Add to blacklist** — Adds a token or deployer address to an AFK config's blacklist (tenant-scoped). list_type selects which list. `POST https://api.stryke.gg/v1/afk/config/{id}/blacklist` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — AFK config id. **Body parameters:** - `list_type` (string, required) — Which blacklist: must be "token" or "dev". - `address` (string, required) — Address to blacklist. Must be non-empty. **Response:** Empty address or list_type not in {token,dev} returns HTTP 200 success:false. > Requires X-User-Ref header. Supports Idempotency-Key. ### DELETE /v1/afk/config/{id}/blacklist **Clear blacklist** — Clears both the token and deployer blacklists for an AFK config (tenant-scoped) in one call. `DELETE https://api.stryke.gg/v1/afk/config/{id}/blacklist` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — AFK config id. **Response:** Clears both token and dev blacklist lists (runs both clears concurrently). > Requires X-User-Ref header. Supports Idempotency-Key. ### GET /v1/afk/config/{id}/blacklist-words **List blacklist words** — Returns the name/symbol blacklist words for an AFK config (tenant-scoped). Tokens whose name/symbol match a word are skipped. `GET https://api.stryke.gg/v1/afk/config/{id}/blacklist-words` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — AFK config id. **Response:** words[] fields: id, word, created_at. count = entries. > Requires X-User-Ref header. ### POST /v1/afk/config/{id}/blacklist-words **Add blacklist words** — Adds one or more words to an AFK config's name/symbol blacklist (tenant-scoped). Returns how many were newly added vs submitted (dedup). `POST https://api.stryke.gg/v1/afk/config/{id}/blacklist-words` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — AFK config id. **Body parameters:** - `words` (string[], required) — Array of words to blacklist. Must be non-empty. **Response:** added = newly inserted rows; submitted = number of words in the request (difference = duplicates skipped). Empty array returns HTTP 200 success:false. > Requires X-User-Ref header. Supports Idempotency-Key. ### DELETE /v1/afk/config/{id}/blacklist-words **Clear blacklist words** — Removes all blacklist words from an AFK config (tenant-scoped). `DELETE https://api.stryke.gg/v1/afk/config/{id}/blacklist-words` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — AFK config id. **Response:** Clears the entire word list for the config. > Requires X-User-Ref header. Supports Idempotency-Key. ### GET /v1/afk/config/{id}/sell-stages **List sell stages** — Returns the laddered auto-sell stages for an AFK config (tenant-scoped). Each stage sells a percentage at a price multiplier. `GET https://api.stryke.gg/v1/afk/config/{id}/sell-stages` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — AFK config id. **Response:** stages[] fields: id, stage_order, sell_pct, multiplier, created_at. count = stages. > Requires X-User-Ref header. ### POST /v1/afk/config/{id}/sell-stages **Add sell stage** — Adds an auto-sell stage (sell_pct at a price multiplier) to an AFK config (tenant-scoped). Validates sell_pct in (0,100] and multiplier > 0. `POST https://api.stryke.gg/v1/afk/config/{id}/sell-stages` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — AFK config id. **Body parameters:** - `sell_pct` (number, required) — Percent of holdings to sell at this stage. Must be > 0 and <= 100. - `multiplier` (number, required) — Price multiplier (e.g. 2.0 = 2x) that triggers this stage. Must be > 0. **Response:** Out-of-range sell_pct or non-positive multiplier returns HTTP 200 success:false. > Requires X-User-Ref header. Supports Idempotency-Key. ### POST /v1/afk/config/{id}/remove-sell-stage **Remove sell stage** — Removes a single auto-sell stage from an AFK config by its stage_order (tenant-scoped). `POST https://api.stryke.gg/v1/afk/config/{id}/remove-sell-stage` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — AFK config id. **Body parameters:** - `stage_order` (int, required) — Order index of the stage to remove. **Response:** Removes the stage matching stage_order. DB error returns HTTP 200 success:false. > Requires X-User-Ref header. Supports Idempotency-Key. POST (not DELETE) because it takes a body. ### DELETE /v1/afk/config/{id}/sell-stages **Clear sell stages** — Removes all auto-sell stages from an AFK config (tenant-scoped). `DELETE https://api.stryke.gg/v1/afk/config/{id}/sell-stages` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — AFK config id. **Response:** Clears every sell stage for the config. > Requires X-User-Ref header. Supports Idempotency-Key. ### GET /v1/afk/config/{id}/smart-wallets **List smart wallets** — Returns the smart-wallet list for an AFK config (tenant-scoped). Used with smart_wallet_threshold to require buys from N tracked wallets before auto-buying. `GET https://api.stryke.gg/v1/afk/config/{id}/smart-wallets` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — AFK config id. **Response:** smart_wallets[] fields: id, address (DB column wallet_address), label (nullable), created_at. count = entries. > Requires X-User-Ref header. ### POST /v1/afk/config/{id}/smart-wallets **Add smart wallet** — Adds a smart wallet (with optional label) to an AFK config's tracked wallet list (tenant-scoped). `POST https://api.stryke.gg/v1/afk/config/{id}/smart-wallets` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — AFK config id. **Body parameters:** - `address` (string, required) — Wallet address to track. Must be non-empty. - `label` (string, optional) — Optional human label for the wallet. **Response:** Empty address returns HTTP 200 success:false. > Requires X-User-Ref header. Supports Idempotency-Key. ### POST /v1/afk/config/{id}/remove-smart-wallet **Remove smart wallet** — Removes a single smart wallet from an AFK config by address (tenant-scoped). `POST https://api.stryke.gg/v1/afk/config/{id}/remove-smart-wallet` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — AFK config id. **Body parameters:** - `address` (string, required) — Wallet address to remove. **Response:** Removes the matching wallet. DB error returns HTTP 200 success:false. > Requires X-User-Ref header. Supports Idempotency-Key. POST (not DELETE) because it takes a body. ### DELETE /v1/afk/config/{id}/smart-wallets **Clear smart wallets** — Removes all smart wallets from an AFK config (tenant-scoped). `DELETE https://api.stryke.gg/v1/afk/config/{id}/smart-wallets` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — AFK config id. **Response:** Clears the entire smart-wallet list for the config. > Requires X-User-Ref header. Supports Idempotency-Key. ## Trading API: Sniper & Auto-Sell Auth: send your key as `Authorization: Bearer ` (the trading API uses Bearer, NOT the X-API-Key header used by the data API). New-launch sniping plus automated take-profit / stop-loss auto-sell. automation scope; per-tenant. ### POST /v1/sniper/create-config **Create sniper config** — Creates a new auto-sniper/TG-Auto config for the calling tenant and immediately marks it saved (is_saved=true). The target wallet is verified to belong to the caller before creation so a tenant cannot snipe-buy through another tenant's wallet. `POST https://api.stryke.gg/v1/sniper/create-config` · auth: `Authorization: Bearer` **Body parameters:** - `name` (string, required) — Display name for the config - `wallet_id` (int, optional) — Wallet to buy with; must belong to caller. Defaults to the caller's most recent active wallet when omitted - `buy_amount` (int, required) — Buy size in lamports; must be positive **Response:** config_id is the new row id; echoes name, wallet_id (resolved), and buy_amount (lamports). > Mutation — supports Idempotency-Key header (automation idempotency layer). ### GET /v1/sniper/configs **List sniper configs** — Returns all sniper/TG-Auto/X-Auto configs owned by the calling tenant. `GET https://api.stryke.gg/v1/sniper/configs` · auth: `Authorization: Bearer` **Response:** configs is an array of full config objects (see config_to_json fields incl. buy_amount lamports + buy_amount_sol, filters JSON, X-Auto monitor flags); count is the array length. ### GET /v1/sniper/config/{id} **Get sniper config** — Returns one sniper config (scoped to the caller) plus its channel count and a human source summary. Returns not-found if the id does not belong to the caller. `GET https://api.stryke.gg/v1/sniper/config/{id}` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — Sniper config ID (caller-owned) **Response:** config is the full config object; channel_count is the number of attached TG channels; source_summary is a derived human label. ### PUT /v1/sniper/config/{id} **Update sniper config fields** — Bulk-updates arbitrary config fields from a string-keyed map; each field is applied individually and the response reports which succeeded vs failed. Config ownership is verified before any update. `PUT https://api.stryke.gg/v1/sniper/config/{id}` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — Sniper config ID (caller-owned) **Body parameters:** - `fields` (object, required) — Map of field name -> string value to apply (e.g. {"buy_slippage_bps":"1500","max_buy_count":"5"}). Must be non-empty **Response:** On full success returns updated_fields. On partial failure returns success:false with both updated_fields and errors (array of "field: message"). > Mutation — supports Idempotency-Key header. Per-field failures return success:false with an errors array (HTTP 200 envelope). ### DELETE /v1/sniper/config/{id} **Delete sniper config** — Deletes a sniper config owned by the caller (ownership enforced at the SQL layer via telegram_id). `DELETE https://api.stryke.gg/v1/sniper/config/{id}` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — Sniper config ID (caller-owned) **Response:** message confirms the deleted config id. > Mutation — supports Idempotency-Key header. ### POST /v1/sniper/config/{id}/toggle **Toggle sniper config active** — Activates or deactivates a sniper config (turns the snipe worker on/off for it). Ownership verified before toggle. `POST https://api.stryke.gg/v1/sniper/config/{id}/toggle` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — Sniper config ID (caller-owned) **Body parameters:** - `active` (boolean, required) — true to activate, false to deactivate **Response:** is_active echoes the requested state. > Mutation — supports Idempotency-Key header. ### POST /v1/sniper/config/{id}/reset-count **Reset sniper buy count** — Resets the config's current_buy_count back to 0 (clears the lifetime buy cap counter so a capped config resumes buying). Ownership verified first. `POST https://api.stryke.gg/v1/sniper/config/{id}/reset-count` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — Sniper config ID (caller-owned) **Response:** Confirms current_buy_count reset to 0 for the config. > Mutation — supports Idempotency-Key header. ### GET /v1/sniper/config/{id}/channels **List sniper config channels** — Lists the Telegram channels attached to a sniper config (the sources it monitors for token mints). `GET https://api.stryke.gg/v1/sniper/config/{id}/channels` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — Sniper config ID (caller-owned) **Response:** channels is an array of channel objects (id, channel_username, is_preset, added_at, resolved_chat_id); count is the length. ### POST /v1/sniper/config/{id}/channels **Add sniper config channel** — Attaches a Telegram channel to a sniper config. The username is normalized (leading @ stripped, lowercased) before storage. `POST https://api.stryke.gg/v1/sniper/config/{id}/channels` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — Sniper config ID (caller-owned) **Body parameters:** - `channel_username` (string, required) — Telegram channel username to monitor (with or without leading @). Required, non-empty - `is_preset` (boolean, optional) — Whether this is a curated preset channel. Defaults to false **Response:** channel_username is echoed in normalized form (no @, lowercased). > Mutation — supports Idempotency-Key header. ### DELETE /v1/sniper/config/{id}/channels **Remove sniper config channel** — Detaches a Telegram channel from a sniper config. Channel identified by username in the request body (normalized: @ stripped, lowercased). `DELETE https://api.stryke.gg/v1/sniper/config/{id}/channels` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — Sniper config ID (caller-owned) **Body parameters:** - `channel_username` (string, required) — Channel username to remove (with or without leading @). Required, non-empty **Response:** removed_channel is echoed in normalized form (no @, lowercased). > Mutation — supports Idempotency-Key header. Channel selector is in the JSON body, not the path. ### GET /v1/sniper/executions **List sniper executions** — Returns sniper execution history (detected/bought/filtered/failed events). With ?config_id it returns that config's history; without it, fans out across only the caller's own configs (user-scoped, never global). `GET https://api.stryke.gg/v1/sniper/executions` · auth: `Authorization: Bearer` **Query parameters:** - `config_id` (int, optional) — Restrict to a single config's executions. When omitted, returns recent executions across all of the caller's configs - `limit` (int, optional) — Max rows to return. Defaults to 20, capped at 100 **Response:** executions is an array of execution objects (see execution_to_json). config_id is only present in the response when supplied in the query. count is the array length. ### PUT /v1/sniper/config/{id}/x-settings **Update sniper X-Auto settings** — Updates X (Twitter) monitoring settings on a sniper config: sets/removes the X handle as a source, sets/clears x_user_id, and toggles monitor_posts/replies/reposts. Each provided field is applied individually with per-field success/error reporting. Ownership verified first. `PUT https://api.stryke.gg/v1/sniper/config/{id}/x-settings` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — Sniper config ID (caller-owned) **Body parameters:** - `x_handle` (string, optional) — X handle to monitor. Empty string removes the X source; non-empty sets it - `x_user_id` (string, optional) — Numeric X user id. Empty string clears it; non-empty sets it - `monitor_posts` (boolean, optional) — Whether to monitor original posts - `monitor_replies` (boolean, optional) — Whether to monitor replies - `monitor_reposts` (boolean, optional) — Whether to monitor reposts **Response:** updated_fields lists applied changes (e.g. "x_handle", "x_handle (removed)", "x_user_id (cleared)"). On partial failure returns success:false with an additional errors array. > Mutation — supports Idempotency-Key header. Per-field failures return success:false with an errors array (HTTP 200 envelope). ### POST /v1/autosell/create **Create AutoSell profile** — Creates a new named AutoSell profile for the calling tenant. The user is resolved from the X-User-Ref header (tenancy); the new profile starts with default rules and returns its numeric id. `POST https://api.stryke.gg/v1/autosell/create` · auth: `Authorization: Bearer` **Body parameters:** - `name` (string, required) — Profile display name. Must be non-empty (trimmed). **Response:** On success returns the new profile_id (i32). On failure returns {"success":false,"error":""} with HTTP 200 (e.g. empty name, DB error). > Mutation under the automation group — supports Idempotency-Key header (tenancy idempotency_layer). Tenant resolved via X-User-Ref. Validation/DB errors return HTTP 200 with success:false. ### GET /v1/autosell/profiles **List AutoSell profiles** — Lists all AutoSell profiles owned by the calling tenant (resolved from X-User-Ref). Returns a count plus the full serialized profile objects. `GET https://api.stryke.gg/v1/autosell/profiles` · auth: `Authorization: Bearer` **Response:** profiles[] is the full profile object (same shape as get/update). count is profiles.len(). Errors return HTTP 200 with success:false. > Tenant resolved via X-User-Ref. Read-only (no idempotency needed). ### GET /v1/autosell/profile/{id} **Get AutoSell profile** — Fetches a single AutoSell profile by id, scoped to the calling tenant (resolved from X-User-Ref). 404-equivalent when the profile does not exist or is not owned by the tenant. `GET https://api.stryke.gg/v1/autosell/profile/{id}` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — AutoSell profile id (i32). **Response:** profile holds all profile fields. When the profile is missing/not owned, returns {"success":false,"error":"Profile not found"} (HTTP 200). > Ownership enforced in DB query (id + tenant user). Not-found returns success:false at HTTP 200. ### PUT /v1/autosell/profile/{id} **Update AutoSell profile** — Partially updates an AutoSell profile (all body fields optional); only provided fields are written. Scoped to the calling tenant (X-User-Ref). Returns the full updated profile. `PUT https://api.stryke.gg/v1/autosell/profile/{id}` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — AutoSell profile id (i32). **Body parameters:** - `name` (string, optional) — New profile name. Must be non-empty (trimmed) if provided. - `fixed_rules` (object, optional) — Fixed TP/SL ladder rules (raw JSON, stored as-is). - `trailing_enabled` (bool, optional) — Enable/disable trailing stop. Triggers a trailing update if any trailing_* field is present. - `trailing_activation_pct` (number, optional) — Profit % at which the trailing stop activates. - `trailing_drawdown_pct` (number, optional) — Drawdown % from peak that triggers the sell. - `moonbag_pct` (int, optional) — Percent of position to retain as a moonbag (not sold). - `expiry_seconds` (int, optional) — Seconds until the profile auto-expires (0 = no expiry). - `sell_slippage_bps` (int, optional) — Sell slippage tolerance in basis points. - `mev_protect` (bool, optional) — Route auto-sells through MEV-protected providers. **Response:** Each provided field is applied via a separate DB update; on any sub-update error returns success:false immediately. On success re-fetches and returns the full updated profile. > Same path as GET/DELETE (multi-method route). Supports Idempotency-Key header (automation idempotency_layer). Empty name, missing tenant, or DB errors return HTTP 200 with success:false. ### DELETE /v1/autosell/profile/{id} **Delete AutoSell profile** — Deletes an AutoSell profile by id, scoped to the calling tenant (X-User-Ref). Returns success:false if no matching owned profile was found. `DELETE https://api.stryke.gg/v1/autosell/profile/{id}` · auth: `Authorization: Bearer` **Path parameters:** - `id` (int, optional) — AutoSell profile id (i32) to delete. **Response:** On a no-op delete (profile missing/not owned) returns {"success":false,"error":"Profile not found"} (HTTP 200). > Same path as GET/PUT (multi-method route). Ownership enforced in the DELETE query (id + tenant user). ### POST /v1/autosell/attach **Attach AutoSell profile** — Attaches an AutoSell profile either as the tenant's global default (source=global) or to a specific AFK config (source=afk, requires source_config_id). All operations scoped to the calling tenant (X-User-Ref). `POST https://api.stryke.gg/v1/autosell/attach` · auth: `Authorization: Bearer` **Body parameters:** - `profile_id` (int, required) — AutoSell profile id to attach. - `source` (string, required) — Attach target. Valid values: "global" (set as tenant default) or "afk" (attach to an AFK config). - `source_config_id` (int, optional) — Target AFK config id. Required when source="afk"; ignored for source="global". **Response:** source=global returns 'set as global default'; source=afk returns 'attached to AFK config #N'. Unknown source, missing source_config_id for afk, or DB errors return {"success":false,"error":"..."} (HTTP 200). > Mutation under automation group — supports Idempotency-Key header. source="afk" without source_config_id, or any source other than global/afk, returns success:false at HTTP 200. ## Trading API: System Auth: send your key as `Authorization: Bearer ` (the trading API uses Bearer, NOT the X-API-Key header used by the data API). Service health and a per-key usage / quota snapshot. ### GET /v1/health **Health check** — Public, unauthenticated liveness/readiness probe. Returns DB reachability plus build/version markers; with ?deep=1 it also probes RPC reachability (cached ~5s) and treasury config. Returns 503 only when the DB is unreachable so load balancers drain the node. `GET https://api.stryke.gg/v1/health` · public **Query parameters:** - `deep` (string, optional) — When set to 1/true/yes/on, also probes RPC reachability (rpc_ok, cached at most once per ~5s) and treasury_configured, and folds rpc_ok into status. Omit for the cheap single DB-ping path. **Response:** data.status is "ok" or "degraded". db_ok = SELECT 1 succeeded. workers_mode is "on"|"off". build_version = bot BOT_VERSION deploy marker; crate_version = CARGO_PKG_VERSION. rpc_ok and treasury_configured only present when ?deep=1; in deep mode status becomes "ok" only if both db_ok && rpc_ok. HTTP 200 whenever db_ok (even if rpc degraded); HTTP 503 only when DB is down. ### GET /v1/usage **Usage / credit snapshot** — App-level credit and quota snapshot for the calling X-API-Key. Free (0 credits), no X-User-Ref / tenancy — reports the key's plan, scopes, owner wallet, and credit window usage. Read scope. `GET https://api.stryke.gg/v1/usage` · auth: `Authorization: Bearer` **Response:** data.credits_monthly is null for unlimited keys (then unlimited=true and credits_remaining is not meaningful). credits_used/credits_remaining are scoped to the rolling window of window_days, resetting at window_resets_at (RFC3339). rate_per_min and rate_burst are the key's token-bucket limits. On unknown app returns 200 with success:false error.code=not_found; on DB error returns 200 with success:false error.code=db. ## Programs On-chain program (smart-contract) intel — account, upgrade state, activity, type mix. ### GET /api/v2/programs/{programId} **Program (smart-contract) intel** — Program account + upgrade state, activity sampling (tx/min, fail%, last-active), a tx-type mix to label an unknown program, and recent decoded transactions. Consolidates a direct-Stryke's node layer program page into one allow-listed call. `GET https://api.stryke.gg/api/v2/programs/{programId}` · auth: `X-API-Key` **Path parameters:** - `programId` (string, optional) — Program (smart-contract) address, base58. **Response:** account{} — executable, owner, loader, dataSize (bytes), lamports/sol, upgradeable, upgradeAuthority, immutable, frozen (=immutable===true), lastDeploySlot, lastDeployTs. label from the known-program table then the static service registry (null if unknown). activity{} sampled from up to 1000 recent signatures: sampled count, txPerMin (over the sampled span, null if no span), failPct (sampled), lastActiveTs (epoch sec), lowActivity (sampled < 50). typeMix[] = enhanced-tx type histogram (over up to 100 txs) sorted by count, with pct. recentTxs[] = up to 25 decoded txs {type, description, feePayer, signature, ts, error}. Cached 120s. ## Positions DeFi position aggregation across Kamino, Drift, MarginFi, Jupiter Perps. ### GET /api/v2/positions/{wallet} **DeFi positions** — Aggregated DeFi positions across Kamino (lend/borrow), Drift (perp + spot), MarginFi (lend/borrow) and Jupiter Perps, flattened into one normalized position contract. Each protocol is fetched independently and isolated — one failing leg never sinks the others. `GET https://api.stryke.gg/api/v2/positions/{wallet}` · auth: `X-API-Key` **Path parameters:** - `wallet` (string, optional) — Solana wallet address (base58). **Response:** positions[] of {protocol ('kamino'|'drift'|'marginfi'|'jupiter_perps'), type ('lend'|'borrow'|'perp'), market (human label), sizeUsd (always ≥0, glitch-capped 1e12), pnlUsd (signed; null where the protocol decode can't derive it — Kamino, MarginFi, Drift spot), leverage (number or null; only Jupiter Perps exposes it), healthFactor (Kamino obligation-level only, else null)}. Kamino USD is i128/2^60 fixed-point; Drift/MarginFi are on-chain borsh decodes; Jupiter Perps is public REST. Empty/dust legs are dropped. Cached 300s. ## Webhooks Push delivery the other way round: instead of holding a connection open, you give us an https endpoint and we POST to it when a trade happens. Same tape as the streaming and REST trade endpoints, read through the same function with the same cursor, so all three agree. THE GUARANTEE IS AT-LEAST-ONCE AND UNORDERED, said plainly because it is what you have to build against: every event carries a stable `eventId` that does not change across retries, so dedupe on it, and every event carries its block and log index, so order on those. Every delivery is signed — X-Stryke-Signature is HMAC-SHA256 over `${t}.${rawBody}` under a secret shown exactly once, at creation. The timestamp is inside the signature, so a captured request cannot be replayed; reject anything more than 300 seconds old. Sign the RAW bytes: re-encoding the parsed JSON changes key order and makes valid signatures fail intermittently. Failed deliveries retry at 10s, 1m, 5m, 30m, 2h and 6h and then dead-letter, and 20 consecutive failures switch the subscription off rather than letting a queue grow behind a dead endpoint — GET /{id}/deliveries shows every attempt and why it failed. Targets must be public https: private, loopback, link-local and cloud-metadata addresses are refused at creation and re-checked before every attempt, and redirects are never followed. A new subscription starts from the moment you created it measured in BLOCK time, so the first event arrives once the index has followed past that instant — up to about 90 seconds on Ethereum, because the follower writes in bursts. That opening silence is expected, not a failure. POST /{id}/test queues a synthetic event through the real path, so you can prove your signature verification works immediately, without waiting for a real trade. Like streaming, this is produced from Stryke's own index, so it covers the chains the index follows — GET /api/v2/stream/chains lists them. ### POST /api/v2/webhooks **Create a webhook** — Subscribe an https endpoint to a token's trades. Returns the signing secret ONCE. `POST https://api.stryke.gg/api/v2/webhooks` · auth: `X-API-Key` **Body parameters:** - `chain` (string, required) — A chain the index follows — see GET /api/v2/stream/chains. - `token` (string, required) — Token address in the chain's own format. - `url` (string, required) — https endpoint to POST to. Must resolve to a public address; private, loopback, link-local and cloud-metadata targets are refused. - `kind` (string, optional) — 'trades' (default and currently the only kind). - `filters` (object, optional) — Optional { minAmountUsd, side, commitment }. An unrecognised filter is a 400, never silently ignored. commitment 'confirmed' (Solana only, where enabled) delivers from the confirmed fast lane ~1–3 s after the block instead of the finalized tape (~15 s), with the same eventId the finalized delivery would carry — so dedupe on eventId as always — and sends a `trade.retracted` event (data.retractedEventId) if that trade does not finalize as delivered — data.finalizedEventId names the event that carries it when the same transaction finalized at another position. A token with more than 200,000 trades in 24 h cannot use 'confirmed'. **Response:** The secret is shown once, in this response, and no route will ever return it again — rotate if it is lost. Verify every delivery: X-Stryke-Signature is `t=,v1=`, where v1 is HMAC-SHA256 of `${t}.${rawBody}` under the subscription secret. Sign the RAW request bytes — re-encoding the parsed JSON changes key order and unicode escaping and makes valid signatures fail intermittently. Reject anything whose `t` is more than 300 seconds from now; the timestamp is inside the signature so a captured request cannot be replayed. > Delivery is AT-LEAST-ONCE and UNORDERED. Dedupe on `eventId`, which is stable across retries, and order using the block and log index each event carries. Retries run 10s, 1m, 5m, 30m, 2h, 6h and then dead-letter; 20 consecutive failures disables the subscription. REORGS: `eventId` is derived from the trade's transaction hash and log index. It is stable across our retries and restarts — the cases you dedupe for — but a chain reorganisation can re-include a transaction under a different log index, and that is a different id. In practice the index reads tape that is already 30-90 seconds old, deeper than reorgs go on the chains it follows. FIRST EVENT: a new subscription starts from the moment you created it, measured in BLOCK time, so nothing arrives until the index has followed past that instant — measured on Ethereum 2026-09-07, up to about 90 seconds, because the follower writes in bursts rather than continuously. That is a silence, not a failure. Use POST /{id}/test to prove your endpoint and your signature check work immediately, without waiting for a real trade. ### GET /api/v2/webhooks **List your webhooks** — Every subscription on this API key. Secrets are never included. `GET https://api.stryke.gg/api/v2/webhooks` · auth: `X-API-Key` ### GET /api/v2/webhooks/{id} **One webhook** — A subscription plus its pending/delivered/dead delivery counts. `GET https://api.stryke.gg/api/v2/webhooks/{id}` · auth: `X-API-Key` **Path parameters:** - `id` (undefined, required) — Webhook id. ### DELETE /api/v2/webhooks/{id} **Delete a webhook** — Removes the subscription and everything still queued for it. `DELETE https://api.stryke.gg/api/v2/webhooks/{id}` · auth: `X-API-Key` **Path parameters:** - `id` (undefined, required) — Webhook id. ### PATCH /api/v2/webhooks/{id} **Enable or disable** — Body { "active": true|false }. Re-enabling also clears the consecutive-failure count. `PATCH https://api.stryke.gg/api/v2/webhooks/{id}` · auth: `X-API-Key` **Path parameters:** - `id` (undefined, required) — Webhook id. **Body parameters:** - `active` (boolean, required) — Whether the subscription should deliver. > A subscription disabled automatically after repeated failures is re-enabled the same way. Two different things resume, and they resume differently. ANYTHING ALREADY QUEUED IS HELD while the subscription is off and delivered when you turn it back on — being switched off does not destroy the backlog. The TAPE position, on the other hand, skips forward if the subscription is more than an hour behind, so events that were never queued are not replayed. So you get what we had already accepted for you, and current events from then on. ### GET /api/v2/webhooks/{id}/deliveries **Delivery log** — The last attempts for this subscription: status, HTTP code, error, and when the next retry is due. `GET https://api.stryke.gg/api/v2/webhooks/{id}/deliveries` · auth: `X-API-Key` **Path parameters:** - `id` (undefined, required) — Webhook id. **Query parameters:** - `status` (string, optional) — pending | delivered | dead - `limit` (integer, optional) — 1-200, default 50. **Response:** Payloads are deliberately not returned — this is a health log, not a second copy of the firehose. ### POST /api/v2/webhooks/{id}/test **Send a test event** — Queues one synthetic event with the same headers and signature as a real one. `POST https://api.stryke.gg/api/v2/webhooks/{id}/test` · auth: `X-API-Key` **Path parameters:** - `id` (undefined, required) — Webhook id. **Response:** 202, because it is queued rather than delivered. It goes through the real outbox — same signing, same retries, same log — so a successful test proves the real path. > Use this to prove your signature verification works before a real trade depends on it. ### POST /api/v2/webhooks/{id}/rotate **Rotate the signing secret** — Issues a new secret and returns it once. The old one stops working immediately. `POST https://api.stryke.gg/api/v2/webhooks/{id}/rotate` · auth: `X-API-Key` **Path parameters:** - `id` (undefined, required) — Webhook id. **Response:** Rotation is immediate and there is no dual-secret window. Every delivery is signed when it is SENT, not when it is queued, so anything still in the queue — and every retry of something already attempted — goes out under the new secret. Switch your receiver over as soon as this returns. ## Streaming (SSE) Push delivery over Server-Sent Events, fed by Stryke's OWN index rather than by a reseller. That is the whole design: the tape and the candles are already ours, written continuously by the followers, so there is no per-token subscription ceiling and — because the events are stored — a reconnect REPLAYS what was missed instead of jumping to now. Send Last-Event-ID (a browser EventSource does this by itself) or ?cursor= to resume; a cursor older than the resume window answers with a `truncated` event naming the gap rather than silently skipping it. The cost of reading our own tables is that streaming only covers the chains the index follows: GET /api/v2/stream/chains says which, whether each is serving, and how far behind it is. Everywhere else answers 501 with the reason. The candle channel builds the OPEN bar on demand through the same function the roll-up uses, so a forming bar is live rather than waiting for the next roll-up pass. ### GET /api/v2/stream/chains **Which chains can be streamed** — The chains the first-party index follows, whether each is currently serving, how far behind it is, and how far back its history reaches. No key required. `GET https://api.stryke.gg/api/v2/stream/chains` · public **Response:** transport is always "sse". `serving:false` carries the reason — chain_not_indexed, no_cursor, follower_error or status_snapshot_stale. lagSeconds is how far behind the follower is, in seconds, or null when that cannot be established. > Streaming reads Stryke's own tables rather than a reseller, so there is no per-token subscription ceiling; the cost is that it only covers the chains the index follows. Each entry also carries `history` — the oldest and newest daily bar that chain holds — which is the range GET /{chain}/token/{address}/price-at can answer over. Read it before building a backfill: the index starts where it started. ### GET /api/v2/stream/{chain}/token/{address}/trades **Live trade tape (SSE)** — Every swap touching this token, pushed as it lands, read from the first-party tape. `GET https://api.stryke.gg/api/v2/stream/{chain}/token/{address}/trades` · auth: `X-API-Key` **Path parameters:** - `chain` (undefined, required) — A chain the index follows — see GET /api/v2/stream/chains - `address` (undefined, required) — Token address in the chain's own format **Query parameters:** - `cursor` (undefined, optional) — Resume position, `--`. A browser EventSource sends this automatically as Last-Event-ID. - `side` (undefined, optional) — buy or sell - `minUsd` (undefined, optional) — Only trades at or above this USD size - `pool` (undefined, optional) — Only this pool's trades - `commitment` (undefined, optional) — finalized (default) or confirmed. confirmed (Solana only, where enabled) pushes from the confirmed lane ~1–3 s after the block, at-most-once, with NO resume (Last-Event-ID is ignored) and `retract` events for trades that do not finalize. **Response:** text/event-stream. Events: `open` (once, with the resume position and cadence), `trade` (one swap; `id` is the cursor to resume from), `idle` (nothing new since the last poll — this is how you tell a quiet market from a wedged stream), `truncated` (the cursor was older than the resume window and events were skipped — it names the gap rather than silently jumping), `error`, `close`. Comment frames (`: heartbeat`) keep proxies from closing an idle connection and are invisible to an event handler. > Amounts are raw integer units as strings. Reconnect with Last-Event-ID to get exactly the events missed — the tape is stored, so a resume is a replay rather than a jump to now. With commitment=confirmed: event ids are `c::` and cannot be resumed from; `retract` names the `:` keys of delivered trades whose block did not finalize; `lane` reports the confirmed lane going unhealthy or recovering (read the finalized stream meanwhile); each trade carries `commitment:"confirmed"`. Errors specific to it: 400 on a non-Solana chain, 501 where it is not enabled, 503 while the lane is unhealthy, 429 `firehose-class` (quote/stable mints and the chain's busiest tokens), `interest-full`, `key-lease-limit`. ### GET /api/v2/stream/{chain}/token/{address}/candles **Live OHLCV (SSE)** — The token's candles, pushed as they form and again as the open bucket moves. `GET https://api.stryke.gg/api/v2/stream/{chain}/token/{address}/candles` · auth: `X-API-Key` **Path parameters:** - `chain` (undefined, required) — A chain the index follows - `address` (undefined, required) — Token address in the chain's own format **Query parameters:** - `interval` (undefined, optional) — Seconds: 60, 300, 900, 3600, 14400 or 86400. Default 60. - `cursor` (undefined, optional) — Resume position; also accepted as Last-Event-ID. **Response:** Each `candle` event carries `final`. false means the bucket is still open and this is an UPDATE of a bar you have already seen — replace it. true means the bucket has closed and will not change — append it. The cursor deliberately does not advance past an open bucket, so a reconnect always sees that bucket's final form. > The candles are the same rows GET /api/v2/chain/{chain}/token/{address}/ohlcv returns, so a chart can backfill with the REST verb and then follow with this one without a seam. ## Chain-generic market data One set of 26 endpoints that works on every chain in the catalog, under /api/v2/chain/{chain}/*. The chain is a required path segment and is never defaulted — pass the Stryke key (eth), an alias (ethereum), an EIP-155 id (1), evm:1, eip155:1, a CAIP-2 id, or the id a provider uses. Which endpoints answer on a given chain depends on that chain's measured capabilities: GET /api/v2/chains/{chain} lists them, and an endpoint a chain cannot serve answers 501 with the evidence — never a 200 with empty data. Every 200 carries the same envelope, including a `freshness` block with the upstream data's own age, so a chain whose indexer has fallen behind is visible rather than silently served as current. The existing Solana endpoints under /api/v2/tokens/* and /api/v2/wallets/* are unchanged. ### GET /api/v2/chain/{chain}/token/{address} **Token summary (any chain)** — Price, market cap, liquidity, 24h volume, holder count, logo and socials for a token on any chain the catalog serves. Requires the `market` capability. `GET https://api.stryke.gg/api/v2/chain/{chain}/token/{address}` · auth: `X-API-Key` **Path parameters:** - `chain` (string, required) — Any identifier the catalog resolves, case-insensitively: the Stryke key (eth), an alias (ethereum), the display name, an EIP-155 id (1), evm:1, eip155:1, a CAIP-2 id, or the id a provider uses (the market-data layer evm:1, the pool index eth, the DEX index ethereum). GET /api/v2/chains lists them all. - `address` (string, required) — Token address in the chain's own format: 0x… on EVM, base58 on Solana, a Move coin type (0x…::module::Type) on Sui and Aptos. Validated before any provider is called; a mismatch is a 400 that names the expected format. **Response:** Every 200 carries the same envelope: chain{key,chainId,caip2,displayName,tier}, the subject (address/wallet/query), data, source{provider,endpoint}, freshness{observedAt,dataAt,ageMs,stale} and degraded[]. `freshness.stale` is true when the provider's own data timestamp is older than this endpoint's budget — that is how a chain whose indexer has fallen behind is told apart from a live one. `degraded[]` names anything that was not ideal (e.g. provider_fallback:the market-data layer→the DEX index). ### GET /api/v2/chain/{chain}/token/{address}/price **Token price (any chain)** — USD price with 24h change, market cap and liquidity where the provider carries them. Requires the `price` capability. `GET https://api.stryke.gg/api/v2/chain/{chain}/token/{address}/price` · auth: `X-API-Key` **Path parameters:** - `chain` (string, required) — Any identifier the catalog resolves, case-insensitively: the Stryke key (eth), an alias (ethereum), the display name, an EIP-155 id (1), evm:1, eip155:1, a CAIP-2 id, or the id a provider uses (the market-data layer evm:1, the pool index eth, the DEX index ethereum). GET /api/v2/chains lists them all. - `address` (string, required) — Token address in the chain's own format: 0x… on EVM, base58 on Solana, a Move coin type (0x…::module::Type) on Sui and Aptos. Validated before any provider is called; a mismatch is a 400 that names the expected format. **Response:** Every 200 carries the same envelope: chain{key,chainId,caip2,displayName,tier}, the subject (address/wallet/query), data, source{provider,endpoint}, freshness{observedAt,dataAt,ageMs,stale} and degraded[]. `freshness.stale` is true when the provider's own data timestamp is older than this endpoint's budget — that is how a chain whose indexer has fallen behind is told apart from a live one. `degraded[]` names anything that was not ideal (e.g. provider_fallback:the market-data layer→the DEX index). ### GET /api/v2/chain/{chain}/token/{address}/ohlcv **OHLCV candles (any chain)** — Ascending [{t,o,h,l,c,v}] candles for the token's deepest pool. Requires the `ohlcv` capability. `GET https://api.stryke.gg/api/v2/chain/{chain}/token/{address}/ohlcv` · auth: `X-API-Key` **Path parameters:** - `chain` (string, required) — Any identifier the catalog resolves, case-insensitively: the Stryke key (eth), an alias (ethereum), the display name, an EIP-155 id (1), evm:1, eip155:1, a CAIP-2 id, or the id a provider uses (the market-data layer evm:1, the pool index eth, the DEX index ethereum). GET /api/v2/chains lists them all. - `address` (string, required) — Token address in the chain's own format: 0x… on EVM, base58 on Solana, a Move coin type (0x…::module::Type) on Sui and Aptos. Validated before any provider is called; a mismatch is a 400 that names the expected format. **Query parameters:** - `period` (string, optional) — 1m, 5m, 15m, 30m, 1h (default), 4h, 12h, 1d or 1w. - `timeframe` (string, optional) — minute|hour|day, paired with aggregate. An alternative to period. - `aggregate` (integer, optional) — Multiplier for timeframe (e.g. timeframe=minute&aggregate=5). 1-1000, default 1. - `limit` (integer, optional) — 1-1000, default 100. - `from` (integer, optional) — Window start: unix seconds, unix ms or ISO-8601. - `to` (integer, optional) — Window end, same formats. - `pool` (string, optional) — Price a specific pool instead of the deepest one. **Response:** Every 200 carries the same envelope: chain{key,chainId,caip2,displayName,tier}, the subject (address/wallet/query), data, source{provider,endpoint}, freshness{observedAt,dataAt,ageMs,stale} and degraded[]. `freshness.stale` is true when the provider's own data timestamp is older than this endpoint's budget — that is how a chain whose indexer has fallen behind is told apart from a live one. `degraded[]` names anything that was not ideal (e.g. provider_fallback:the market-data layer→the DEX index). > Providers differ in granularity. When the source cannot serve the requested interval the candles are re-bucketed and `degraded` carries resolution_coarser_than_requested. ### GET /api/v2/chain/{chain}/token/{address}/price-at **Price at a point in time** — What this token was worth on a given day, or at a given instant. The question every tax tool, accounting integration and backtest asks first. `GET https://api.stryke.gg/api/v2/chain/{chain}/token/{address}/price-at` · auth: `X-API-Key` **Path parameters:** - `chain` (string, required) — Any identifier the catalog resolves, case-insensitively: the Stryke key (eth), an alias (ethereum), the display name, an EIP-155 id (1), evm:1, eip155:1, a CAIP-2 id, or the id a provider uses (the market-data layer evm:1, the pool index eth, the DEX index ethereum). GET /api/v2/chains lists them all. - `address` (string, required) — Token address in the chain's own format: 0x… on EVM, base58 on Solana, a Move coin type (0x…::module::Type) on Sui and Aptos. Validated before any provider is called; a mismatch is a 400 that names the expected format. **Query parameters:** - `date` (string, optional) — YYYY-MM-DD (UTC). Resolves to that calendar day's CLOSE. - `at` (string, optional) — A unix timestamp in ms or an ISO-8601 instant. Resolves to the bar it falls in. - `interval` (integer, optional) — With `at`: 60 (default) or 3600 seconds. **Response:** PASS date OR at, NEVER BOTH — they are different questions, and answering one with the other is how a tax figure ends up a day out. The answer reads a stored CANDLE, not a spot row: idx_token_price keeps one mutable row per token and has no history, so a candle is the only thing that gives the same answer twice. If the token did not trade in the exact bucket, the last bucket it did trade in is used, `bucketsBack` says how far back that was, and `degraded` carries resolved_to_earlier_bucket: — so a caller can decide whether a three-day-old close is good enough. > Covers the chains the index follows — GET /api/v2/stream/chains lists them, and each entry carries `history.oldest`, the earliest bar that chain has. Check it before backfilling: the index starts where it started, and asking for an earlier date is a 404 no retry will fix. A settled historical bar never changes, so this is cached for an hour. ### GET /api/v2/chain/{chain}/token/{address}/trades **Recent trades (any chain)** — The live tape for the token's deepest pool: [{ts,type,priceUsd,amountToken,amountUsd,trader,txHash,pool}]. Requires the `trades` capability. `GET https://api.stryke.gg/api/v2/chain/{chain}/token/{address}/trades` · auth: `X-API-Key` **Path parameters:** - `chain` (string, required) — Any identifier the catalog resolves, case-insensitively: the Stryke key (eth), an alias (ethereum), the display name, an EIP-155 id (1), evm:1, eip155:1, a CAIP-2 id, or the id a provider uses (the market-data layer evm:1, the pool index eth, the DEX index ethereum). GET /api/v2/chains lists them all. - `address` (string, required) — Token address in the chain's own format: 0x… on EVM, base58 on Solana, a Move coin type (0x…::module::Type) on Sui and Aptos. Validated before any provider is called; a mismatch is a 400 that names the expected format. **Query parameters:** - `limit` (integer, optional) — 1-500, default 50. **Response:** Every 200 carries the same envelope: chain{key,chainId,caip2,displayName,tier}, the subject (address/wallet/query), data, source{provider,endpoint}, freshness{observedAt,dataAt,ageMs,stale} and degraded[]. `freshness.stale` is true when the provider's own data timestamp is older than this endpoint's budget — that is how a chain whose indexer has fallen behind is told apart from a live one. `degraded[]` names anything that was not ideal (e.g. provider_fallback:the market-data layer→the DEX index). ### GET /api/v2/chain/{chain}/token/{address}/pools **Pools / pairs (any chain)** — Every pool the token trades in, deepest first: [{pool,dex,quoteSymbol,liquidityUsd,volume24h,priceUsd,createdAt}]. Requires the `pools` capability. `GET https://api.stryke.gg/api/v2/chain/{chain}/token/{address}/pools` · auth: `X-API-Key` **Path parameters:** - `chain` (string, required) — Any identifier the catalog resolves, case-insensitively: the Stryke key (eth), an alias (ethereum), the display name, an EIP-155 id (1), evm:1, eip155:1, a CAIP-2 id, or the id a provider uses (the market-data layer evm:1, the pool index eth, the DEX index ethereum). GET /api/v2/chains lists them all. - `address` (string, required) — Token address in the chain's own format: 0x… on EVM, base58 on Solana, a Move coin type (0x…::module::Type) on Sui and Aptos. Validated before any provider is called; a mismatch is a 400 that names the expected format. **Query parameters:** - `limit` (integer, optional) — 1-100, default 20. **Response:** Every 200 carries the same envelope: chain{key,chainId,caip2,displayName,tier}, the subject (address/wallet/query), data, source{provider,endpoint}, freshness{observedAt,dataAt,ageMs,stale} and degraded[]. `freshness.stale` is true when the provider's own data timestamp is older than this endpoint's budget — that is how a chain whose indexer has fallen behind is told apart from a live one. `degraded[]` names anything that was not ideal (e.g. provider_fallback:the market-data layer→the DEX index). ### GET /api/v2/chain/{chain}/token/{address}/holders **Holder distribution (any chain)** — Holder count, top holders and concentration (top10/50/100), plus sniper/bundler/insider counts where the provider computes them. Requires the `holders` capability. `GET https://api.stryke.gg/api/v2/chain/{chain}/token/{address}/holders` · auth: `X-API-Key` **Path parameters:** - `chain` (string, required) — Any identifier the catalog resolves, case-insensitively: the Stryke key (eth), an alias (ethereum), the display name, an EIP-155 id (1), evm:1, eip155:1, a CAIP-2 id, or the id a provider uses (the market-data layer evm:1, the pool index eth, the DEX index ethereum). GET /api/v2/chains lists them all. - `address` (string, required) — Token address in the chain's own format: 0x… on EVM, base58 on Solana, a Move coin type (0x…::module::Type) on Sui and Aptos. Validated before any provider is called; a mismatch is a 400 that names the expected format. **Query parameters:** - `limit` (integer, optional) — 1-500, default 50. **Response:** Every 200 carries the same envelope: chain{key,chainId,caip2,displayName,tier}, the subject (address/wallet/query), data, source{provider,endpoint}, freshness{observedAt,dataAt,ageMs,stale} and degraded[]. `freshness.stale` is true when the provider's own data timestamp is older than this endpoint's budget — that is how a chain whose indexer has fallen behind is told apart from a live one. `degraded[]` names anything that was not ideal (e.g. provider_fallback:the market-data layer→the DEX index). ### GET /api/v2/chain/{chain}/token/{address}/security **Token security (any chain)** — Honeypot verdict, buy/sell tax, mint/freeze authority, ownership renouncement, proxy, LP lock/burn and a 0-100 score where HIGHER IS SAFER. Requires the `security` capability. `GET https://api.stryke.gg/api/v2/chain/{chain}/token/{address}/security` · auth: `X-API-Key` **Path parameters:** - `chain` (string, required) — Any identifier the catalog resolves, case-insensitively: the Stryke key (eth), an alias (ethereum), the display name, an EIP-155 id (1), evm:1, eip155:1, a CAIP-2 id, or the id a provider uses (the market-data layer evm:1, the pool index eth, the DEX index ethereum). GET /api/v2/chains lists them all. - `address` (string, required) — Token address in the chain's own format: 0x… on EVM, base58 on Solana, a Move coin type (0x…::module::Type) on Sui and Aptos. Validated before any provider is called; a mismatch is a 400 that names the expected format. **Response:** Every 200 carries the same envelope: chain{key,chainId,caip2,displayName,tier}, the subject (address/wallet/query), data, source{provider,endpoint}, freshness{observedAt,dataAt,ageMs,stale} and degraded[]. `freshness.stale` is true when the provider's own data timestamp is older than this endpoint's budget — that is how a chain whose indexer has fallen behind is told apart from a live one. `degraded[]` names anything that was not ideal (e.g. provider_fallback:the market-data layer→the DEX index). > `analysed` is false — and every verdict field is null — when the provider has not actually analysed the token; `status` is one of ok, trusted, partial, not_found. `honeypot: null` means the provider could not simulate a sell, NOT that the token is safe. Never read a 0 tax on an unanalysed token as "no tax". ### GET /api/v2/chain/{chain}/token/{address}/metadata **Token metadata (any chain)** — Name, symbol, decimals, logo, description, socials, categories, deployer and creation time. Requires the `metadata` capability. `GET https://api.stryke.gg/api/v2/chain/{chain}/token/{address}/metadata` · auth: `X-API-Key` **Path parameters:** - `chain` (string, required) — Any identifier the catalog resolves, case-insensitively: the Stryke key (eth), an alias (ethereum), the display name, an EIP-155 id (1), evm:1, eip155:1, a CAIP-2 id, or the id a provider uses (the market-data layer evm:1, the pool index eth, the DEX index ethereum). GET /api/v2/chains lists them all. - `address` (string, required) — Token address in the chain's own format: 0x… on EVM, base58 on Solana, a Move coin type (0x…::module::Type) on Sui and Aptos. Validated before any provider is called; a mismatch is a 400 that names the expected format. **Response:** Every 200 carries the same envelope: chain{key,chainId,caip2,displayName,tier}, the subject (address/wallet/query), data, source{provider,endpoint}, freshness{observedAt,dataAt,ageMs,stale} and degraded[]. `freshness.stale` is true when the provider's own data timestamp is older than this endpoint's budget — that is how a chain whose indexer has fallen behind is told apart from a live one. `degraded[]` names anything that was not ideal (e.g. provider_fallback:the market-data layer→the DEX index). ### GET /api/v2/chain/{chain}/address/{address} **What is this address?** — Classifies an address as a token mint, a market (pool) or a wallet, from Stryke's own index — no third-party call. Returns `kind` plus whichever of `token`, `pool` or `wallet` applies. `kind:"unknown"` means the address is not in the index, which is NOT a statement that it does not exist on chain; `checked` names the tables that were searched. `GET https://api.stryke.gg/api/v2/chain/{chain}/address/{address}` · auth: `X-API-Key` **Path parameters:** - `chain` (string, required) — Any identifier the catalog resolves, case-insensitively: the Stryke key (eth), an alias (ethereum), the display name, an EIP-155 id (1), evm:1, eip155:1, a CAIP-2 id, or the id a provider uses (the market-data layer evm:1, the pool index eth, the DEX index ethereum). GET /api/v2/chains lists them all. - `address` (string, required) — Token address in the chain's own format: 0x… on EVM, base58 on Solana, a Move coin type (0x…::module::Type) on Sui and Aptos. Validated before any provider is called; a mismatch is a 400 that names the expected format. **Response:** Every 200 carries the same envelope: chain{key,chainId,caip2,displayName,tier}, the subject (address/wallet/query), data, source{provider,endpoint}, freshness{observedAt,dataAt,ageMs,stale} and degraded[]. `freshness.stale` is true when the provider's own data timestamp is older than this endpoint's budget — that is how a chain whose indexer has fallen behind is told apart from a live one. `degraded[]` names anything that was not ideal (e.g. provider_fallback:the market-data layer→the DEX index). > Only on chains the first-party index follows; elsewhere it is a 501 that says so. ### GET /api/v2/chain/{chain}/token/{address}/allowances **ERC-20 approvals for a wallet** — How much of this token an owner has approved each spender to move. The primitive behind every revoke tool. `GET https://api.stryke.gg/api/v2/chain/{chain}/token/{address}/allowances` · auth: `X-API-Key` **Path parameters:** - `chain` (string, required) — Any identifier the catalog resolves, case-insensitively: the Stryke key (eth), an alias (ethereum), the display name, an EIP-155 id (1), evm:1, eip155:1, a CAIP-2 id, or the id a provider uses (the market-data layer evm:1, the pool index eth, the DEX index ethereum). GET /api/v2/chains lists them all. - `address` (string, required) — The ERC-20 token contract. **Query parameters:** - `owner` (string, required) — The wallet whose approvals you are reading. - `spenders` (string, required) — Comma-separated spender addresses, at most 25. ERC-20 cannot be asked "who holds an approval" — allowance() answers about one pair and the token publishes no list — so the spenders come from you. **Response:** One row per spender: {spender, decimals, raw, allowance}. `raw` is the uint256 in the token's own smallest units AS A STRING — never a float. A null `raw` means the call did not return a uint256, which happens when the contract has no allowance() and falls through to a payable fallback (WETH does exactly this): that is NOT an allowance of zero, and treating it as one would be the safest-sounding wrong answer here. > EVM-only, and only on chains with a verified JSON-RPC endpoint — the same set /gas and /tx serve. ### POST /api/v2/chain/{chain}/tokens/prices **Batch token prices — one chain, or a basket across several** — Prices up to 50 tokens in one call. With a named chain the body is a plain array of addresses. With chain=all each entry names its own chain, so a portfolio spanning several chains is ONE request instead of one per chain. `POST https://api.stryke.gg/api/v2/chain/{chain}/tokens/prices` · auth: `X-API-Key` **Path parameters:** - `chain` (string, required) — Any identifier the catalog resolves, case-insensitively: the Stryke key (eth), an alias (ethereum), the display name, an EIP-155 id (1), evm:1, eip155:1, a CAIP-2 id, or the id a provider uses (the market-data layer evm:1, the pool index eth, the DEX index ethereum). GET /api/v2/chains lists them all. **Body parameters:** - `addresses` (array, required) — Up to 50. On a named chain: addresses as strings. On chain=all: objects { chain, address }, spanning at most 10 distinct chains. **Response:** On a named chain this is the usual envelope. On chain=all the body is { success, chains[], requested, priced, data, sources, errors[] }: `data` is keyed ":
" because the same address exists on many chains and a bare address key would collide. A CHAIN THAT FAILS DOES NOT FAIL THE BASKET — its reason lands in errors[] and everything else still prices, which is the only useful behaviour for a batch. `sources` names the provider that answered per chain. Groups run concurrently, so a basket costs the slowest chain rather than the sum. > Deduplicated per chain before the upstream call. ### GET /api/v2/chain/{chain}/search **Search tokens on a chain** — Token search scoped to one chain, or across every indexed chain with chain=all. `GET https://api.stryke.gg/api/v2/chain/{chain}/search` · auth: `X-API-Key` **Path parameters:** - `chain` (string, required) — Any identifier the catalog resolves, case-insensitively: the Stryke key (eth), an alias (ethereum), the display name, an EIP-155 id (1), evm:1, eip155:1, a CAIP-2 id, or the id a provider uses (the market-data layer evm:1, the pool index eth, the DEX index ethereum). GET /api/v2/chains lists them all. **Query parameters:** - `q` (string, required) — 2-64 characters. - `limit` (integer, optional) — 1-100, default 20. **Response:** Every 200 carries the same envelope: chain{key,chainId,caip2,displayName,tier}, the subject (address/wallet/query), data, source{provider,endpoint}, freshness{observedAt,dataAt,ageMs,stale} and degraded[]. `freshness.stale` is true when the provider's own data timestamp is older than this endpoint's budget — that is how a chain whose indexer has fallen behind is told apart from a live one. `degraded[]` names anything that was not ideal (e.g. provider_fallback:the market-data layer→the DEX index). ### GET /api/v2/chain/{chain}/trending **Trending tokens on a chain** — Top tokens on the chain by recent volume. `GET https://api.stryke.gg/api/v2/chain/{chain}/trending` · auth: `X-API-Key` **Path parameters:** - `chain` (string, required) — Any identifier the catalog resolves, case-insensitively: the Stryke key (eth), an alias (ethereum), the display name, an EIP-155 id (1), evm:1, eip155:1, a CAIP-2 id, or the id a provider uses (the market-data layer evm:1, the pool index eth, the DEX index ethereum). GET /api/v2/chains lists them all. **Query parameters:** - `limit` (integer, optional) — 1-100, default 20. **Response:** Every 200 carries the same envelope: chain{key,chainId,caip2,displayName,tier}, the subject (address/wallet/query), data, source{provider,endpoint}, freshness{observedAt,dataAt,ageMs,stale} and degraded[]. `freshness.stale` is true when the provider's own data timestamp is older than this endpoint's budget — that is how a chain whose indexer has fallen behind is told apart from a live one. `degraded[]` names anything that was not ideal (e.g. provider_fallback:the market-data layer→the DEX index). ### GET /api/v2/chain/{chain}/screener **Screen the chain's tokens** — Rank every token on the chain by what it actually did over a window — volume, trade count and price change — with thresholds. This is the "show me tokens over $50k of volume with more than 200 trades today" question that /trending cannot answer, because /trending is a fixed ranking and this is a query. `GET https://api.stryke.gg/api/v2/chain/{chain}/screener` · auth: `X-API-Key` **Path parameters:** - `chain` (string, required) — Any identifier the catalog resolves, case-insensitively: the Stryke key (eth), an alias (ethereum), the display name, an EIP-155 id (1), evm:1, eip155:1, a CAIP-2 id, or the id a provider uses (the market-data layer evm:1, the pool index eth, the DEX index ethereum). GET /api/v2/chains lists them all. **Query parameters:** - `window` (string, optional) — 1h, 4h, 24h, 1d or 7d. Default 24h. The window picks the bar size that tiles it: 1h/4h/24h read hourly bars, 1d/7d read daily ones. - `minVolumeUsd` (number, optional) — Only tokens whose summed USD volume over the window is at least this. - `minTrades` (integer, optional) — Only tokens with at least this many trades over the window. - `sort` (string, optional) — volume (default), trades, buckets or recent. - `order` (string, optional) — desc (default) or asc. - `limit` (integer, optional) — 1-500, default 50. **Response:** Rows carry volumeUsd, volumeToken, trades, buckets (how many bars actually had data — fewer than the window implies a token that was quiet or newly listed), open, close, changePct and the first/last bucket timestamps. `source.provider` is always "stryke": these numbers are summed from our own tape, not taken from a reseller, which is why there is no ceiling on how many tokens can be ranked. > Reads idx_candle, so it covers the chains the followers follow rather than the whole catalog — GET /api/v2/stream/chains lists them. A chain outside that set answers 501 with the reason rather than a partial ranking from somewhere else. Measured 2026-09-07: 763 ms over 947k candle rows on Solana. ### GET /api/v2/chain/{chain}/wallet/{wallet}/portfolio **Wallet portfolio (any chain, or all)** — Token holdings and total USD value for an address. Use chain=all to fetch every chain the provider indexes in one call. `GET https://api.stryke.gg/api/v2/chain/{chain}/wallet/{wallet}/portfolio` · auth: `X-API-Key` **Path parameters:** - `chain` (string, required) — Any identifier the catalog resolves, case-insensitively: the Stryke key (eth), an alias (ethereum), the display name, an EIP-155 id (1), evm:1, eip155:1, a CAIP-2 id, or the id a provider uses (the market-data layer evm:1, the pool index eth, the DEX index ethereum). GET /api/v2/chains lists them all. - `wallet` (string, required) — Wallet address in the chain's format. **Response:** Every 200 carries the same envelope: chain{key,chainId,caip2,displayName,tier}, the subject (address/wallet/query), data, source{provider,endpoint}, freshness{observedAt,dataAt,ageMs,stale} and degraded[]. `freshness.stale` is true when the provider's own data timestamp is older than this endpoint's budget — that is how a chain whose indexer has fallen behind is told apart from a live one. `degraded[]` names anything that was not ideal (e.g. provider_fallback:the market-data layer→the DEX index). > chain=all is materially slower than one chain (measured 2026-09-02: 3.1s/72KB against 0.2s/2KB) and is therefore opt-in rather than the default. ### GET /api/v2/chain/{chain}/wallet/{wallet}/trades **Wallet trades (any chain)** — Every swap this wallet made on this chain, newest first: token in/out, amounts, USD value, price and transaction hash. `GET https://api.stryke.gg/api/v2/chain/{chain}/wallet/{wallet}/trades` · auth: `X-API-Key` **Path parameters:** - `chain` (string, required) — Any identifier the catalog resolves, case-insensitively: the Stryke key (eth), an alias (ethereum), the display name, an EIP-155 id (1), evm:1, eip155:1, a CAIP-2 id, or the id a provider uses (the market-data layer evm:1, the pool index eth, the DEX index ethereum). GET /api/v2/chains lists them all. - `wallet` (string, required) — Wallet address. **Query parameters:** - `limit` (integer, optional) — 1-500, default 50. **Response:** Every 200 carries the same envelope: chain{key,chainId,caip2,displayName,tier}, the subject (address/wallet/query), data, source{provider,endpoint}, freshness{observedAt,dataAt,ageMs,stale} and degraded[]. `freshness.stale` is true when the provider's own data timestamp is older than this endpoint's budget — that is how a chain whose indexer has fallen behind is told apart from a live one. `degraded[]` names anything that was not ideal (e.g. provider_fallback:the market-data layer→the DEX index). ### GET /api/v2/chain/{chain}/wallet/{wallet}/activity **Wallet activity (any chain)** — The wallet's full on-chain activity on this chain — transfers in and out, swaps, approvals and contract interactions — with spam filtered out. `GET https://api.stryke.gg/api/v2/chain/{chain}/wallet/{wallet}/activity` · auth: `X-API-Key` **Path parameters:** - `chain` (string, required) — Any identifier the catalog resolves, case-insensitively: the Stryke key (eth), an alias (ethereum), the display name, an EIP-155 id (1), evm:1, eip155:1, a CAIP-2 id, or the id a provider uses (the market-data layer evm:1, the pool index eth, the DEX index ethereum). GET /api/v2/chains lists them all. - `wallet` (string, required) — Wallet address. **Query parameters:** - `limit` (integer, optional) — 1-500, default 50. **Response:** Every 200 carries the same envelope: chain{key,chainId,caip2,displayName,tier}, the subject (address/wallet/query), data, source{provider,endpoint}, freshness{observedAt,dataAt,ageMs,stale} and degraded[]. `freshness.stale` is true when the provider's own data timestamp is older than this endpoint's budget — that is how a chain whose indexer has fallen behind is told apart from a live one. `degraded[]` names anything that was not ideal (e.g. provider_fallback:the market-data layer→the DEX index). ### GET /api/v2/chain/{chain}/wallet/{wallet}/pnl **Wallet PnL (any chain)** — Per-token profit and loss for this wallet on this chain: realised and unrealised PnL, average cost basis, amount still held and current value. `GET https://api.stryke.gg/api/v2/chain/{chain}/wallet/{wallet}/pnl` · auth: `X-API-Key` **Path parameters:** - `chain` (string, required) — Any identifier the catalog resolves, case-insensitively: the Stryke key (eth), an alias (ethereum), the display name, an EIP-155 id (1), evm:1, eip155:1, a CAIP-2 id, or the id a provider uses (the market-data layer evm:1, the pool index eth, the DEX index ethereum). GET /api/v2/chains lists them all. - `wallet` (string, required) — Wallet address. **Response:** Every 200 carries the same envelope: chain{key,chainId,caip2,displayName,tier}, the subject (address/wallet/query), data, source{provider,endpoint}, freshness{observedAt,dataAt,ageMs,stale} and degraded[]. `freshness.stale` is true when the provider's own data timestamp is older than this endpoint's budget — that is how a chain whose indexer has fallen behind is told apart from a live one. `degraded[]` names anything that was not ideal (e.g. provider_fallback:the market-data layer→the DEX index). ### GET /api/v2/chain/{chain}/wallet/{wallet}/risk **Wallet risk flags** — Whether this ADDRESS is known to be malicious — sanctioned, a phishing or theft address, a mixer, blacklisted, or the deployer of malicious contracts. The token security verb answers the same kind of question about a contract; this one is about the counterparty. `GET https://api.stryke.gg/api/v2/chain/{chain}/wallet/{wallet}/risk` · auth: `X-API-Key` **Path parameters:** - `chain` (string, required) — Any identifier the catalog resolves, case-insensitively: the Stryke key (eth), an alias (ethereum), the display name, an EIP-155 id (1), evm:1, eip155:1, a CAIP-2 id, or the id a provider uses (the market-data layer evm:1, the pool index eth, the DEX index ethereum). GET /api/v2/chains lists them all. - `wallet` (string, required) — The address to screen, in the chain's format. **Response:** THE THREE OUTCOMES ARE DIFFERENT AND YOU MUST BRANCH ON ALL THREE. riskLevel "none" with a score of 100 means the address was checked and nothing was found. riskLevel "unknown" with score null and status "not_analysed" means NOBODY HAS LOOKED — flagsResolved is 0 — and it is not a clearance. Anything else names what was raised in `flags`. Treating unknown as clean is the failure mode this endpoint exists to prevent. > Chain-scoped and never chain=all: a sanctions or phishing flag is a claim about an address ON a chain, and answering from whichever chain replied first would be exactly the class of bug this namespace exists to prevent. ### GET /api/v2/chain/{chain}/wallet/{wallet}/defi-positions **Wallet DeFi positions (any chain)** — Liquidity-pool, lending and staking positions this wallet holds on this chain, with the protocol, the underlying assets and the position's USD value. `GET https://api.stryke.gg/api/v2/chain/{chain}/wallet/{wallet}/defi-positions` · auth: `X-API-Key` **Path parameters:** - `chain` (string, required) — Any identifier the catalog resolves, case-insensitively: the Stryke key (eth), an alias (ethereum), the display name, an EIP-155 id (1), evm:1, eip155:1, a CAIP-2 id, or the id a provider uses (the market-data layer evm:1, the pool index eth, the DEX index ethereum). GET /api/v2/chains lists them all. - `wallet` (string, required) — Wallet address. **Response:** Every 200 carries the same envelope: chain{key,chainId,caip2,displayName,tier}, the subject (address/wallet/query), data, source{provider,endpoint}, freshness{observedAt,dataAt,ageMs,stale} and degraded[]. `freshness.stale` is true when the provider's own data timestamp is older than this endpoint's budget — that is how a chain whose indexer has fallen behind is told apart from a live one. `degraded[]` names anything that was not ideal (e.g. provider_fallback:the market-data layer→the DEX index). ### GET /api/v2/chain/{chain}/wallet/{wallet}/history **Wallet balance history (any chain)** — Total USD value of this wallet on this chain over time, as a time series suitable for charting a portfolio curve. `GET https://api.stryke.gg/api/v2/chain/{chain}/wallet/{wallet}/history` · auth: `X-API-Key` **Path parameters:** - `chain` (string, required) — Any identifier the catalog resolves, case-insensitively: the Stryke key (eth), an alias (ethereum), the display name, an EIP-155 id (1), evm:1, eip155:1, a CAIP-2 id, or the id a provider uses (the market-data layer evm:1, the pool index eth, the DEX index ethereum). GET /api/v2/chains lists them all. - `wallet` (string, required) — Wallet address. **Query parameters:** - `period` (string, optional) — 1h, 1d, 7d, 30d, 90d or 365d. - `from` (integer, optional) — Window start. - `to` (integer, optional) — Window end. **Response:** Every 200 carries the same envelope: chain{key,chainId,caip2,displayName,tier}, the subject (address/wallet/query), data, source{provider,endpoint}, freshness{observedAt,dataAt,ageMs,stale} and degraded[]. `freshness.stale` is true when the provider's own data timestamp is older than this endpoint's budget — that is how a chain whose indexer has fallen behind is told apart from a live one. `degraded[]` names anything that was not ideal (e.g. provider_fallback:the market-data layer→the DEX index). ### GET /api/v2/chain/{chain}/quote **Swap quote — non-custodial (any chain)** — An UNSIGNED swap quote from an aggregator (the routing layer) or, on registry chains, the chain's own Uniswap-family quoter. Returns amounts, price impact, gas estimate, the route and — where the aggregator provides one — an unsigned transactionRequest. `GET https://api.stryke.gg/api/v2/chain/{chain}/quote` · auth: `X-API-Key` **Path parameters:** - `chain` (string, required) — Any identifier the catalog resolves, case-insensitively: the Stryke key (eth), an alias (ethereum), the display name, an EIP-155 id (1), evm:1, eip155:1, a CAIP-2 id, or the id a provider uses (the market-data layer evm:1, the pool index eth, the DEX index ethereum). GET /api/v2/chains lists them all. **Query parameters:** - `from` (string, required) — Input token address, or `native` for the chain's native asset. - `to` (string, required) — Output token address, or `native`. - `amount` (string, optional) — Input amount in the SMALLEST units of the input token; digits only, 1-78 of them (uint256 max is 78 decimal digits). Either this or amountHuman. - `amountHuman` (string, optional) — Input amount as a decimal (e.g. 0.01); converted exactly using the token's decimals. At most 78 integer and 36 fractional digits. - `slippageBps` (integer, optional) — 1-5000, default 100 (1%). - `order` (string, optional) — Route preference: RECOMMENDED, FASTEST, CHEAPEST or SAFEST (case-insensitive). Anything else is a 400. - `fromAddress` (string, optional) — The sender. Without it a placeholder is used and transactionRequest is omitted. - `toAddress` (string, optional) — Recipient, when it differs from the sender. Validated against the DESTINATION chain's address format (toChain when given, otherwise this chain); a mismatch is a 400. - `toChain` (string, optional) — Destination chain for a cross-chain quote; any identifier GET /api/v2/chains resolves. A destination whose measured `quote` capability is false answers 501 rather than silently becoming a same-chain quote. **Response:** Every 200 carries the same envelope: chain{key,chainId,caip2,displayName,tier}, the subject (address/wallet/query), data, source{provider,endpoint}, freshness{observedAt,dataAt,ageMs,stale} and degraded[]. `freshness.stale` is true when the provider's own data timestamp is older than this endpoint's budget — that is how a chain whose indexer has fallen behind is told apart from a live one. `degraded[]` names anything that was not ideal (e.g. provider_fallback:the market-data layer→the DEX index). > THIS ENDPOINT NEVER SIGNS OR SUBMITS. The response carries custody:"none" and a note saying so; signing and submission are the caller's. Custodial execution is a separate, allowlisted surface (/api/v2/trade/send). ### GET /api/v2/chain/{chain}/gas **Gas price and L1 data fee (EVM chains)** — gasPrice, base fee, priority fee and — on rollups that charge one — the L1 data fee, read from the chain's own JSON-RPC and the rollup's fee oracle. `GET https://api.stryke.gg/api/v2/chain/{chain}/gas` · auth: `X-API-Key` **Path parameters:** - `chain` (string, required) — Any identifier the catalog resolves, case-insensitively: the Stryke key (eth), an alias (ethereum), the display name, an EIP-155 id (1), evm:1, eip155:1, a CAIP-2 id, or the id a provider uses (the market-data layer evm:1, the pool index eth, the DEX index ethereum). GET /api/v2/chains lists them all. **Query parameters:** - `gas` (integer, optional) — Gas units to cost out; the response multiplies the price by this. 1-30000000. **Response:** Every 200 carries the same envelope: chain{key,chainId,caip2,displayName,tier}, the subject (address/wallet/query), data, source{provider,endpoint}, freshness{observedAt,dataAt,ageMs,stale} and degraded[]. `freshness.stale` is true when the provider's own data timestamp is older than this endpoint's budget — that is how a chain whose indexer has fallen behind is told apart from a live one. `degraded[]` names anything that was not ideal (e.g. provider_fallback:the market-data layer→the DEX index). > EVM chains that Stryke's engine has a verified RPC endpoint for. Everything else answers 501. ### GET /api/v2/chain/{chain}/tx/{hash} **Transaction receipt (EVM chains)** — Receipt, status, gas used and the L1 data fee where the chain charges one. `GET https://api.stryke.gg/api/v2/chain/{chain}/tx/{hash}` · auth: `X-API-Key` **Path parameters:** - `chain` (string, required) — Any identifier the catalog resolves, case-insensitively: the Stryke key (eth), an alias (ethereum), the display name, an EIP-155 id (1), evm:1, eip155:1, a CAIP-2 id, or the id a provider uses (the market-data layer evm:1, the pool index eth, the DEX index ethereum). GET /api/v2/chains lists them all. - `hash` (string, required) — 0x-prefixed 32-byte transaction hash. **Response:** Every 200 carries the same envelope: chain{key,chainId,caip2,displayName,tier}, the subject (address/wallet/query), data, source{provider,endpoint}, freshness{observedAt,dataAt,ageMs,stale} and degraded[]. `freshness.stale` is true when the provider's own data timestamp is older than this endpoint's budget — that is how a chain whose indexer has fallen behind is told apart from a live one. `degraded[]` names anything that was not ideal (e.g. provider_fallback:the market-data layer→the DEX index). > Solana transaction decoding is served by POST /api/v2/wallets/decode-tx, which understands instructions and inner instructions; this endpoint answers 501 for svm chains and says so. ### GET /api/v2/chain/{chain}/capabilities **What this chain can do** — The catalog row for one chain: measured tier, per-capability booleans, the provider names covering it, address format, explorer templates and the prober's evidence. `GET https://api.stryke.gg/api/v2/chain/{chain}/capabilities` · auth: `X-API-Key` **Path parameters:** - `chain` (string, required) — Any identifier the catalog resolves, case-insensitively: the Stryke key (eth), an alias (ethereum), the display name, an EIP-155 id (1), evm:1, eip155:1, a CAIP-2 id, or the id a provider uses (the market-data layer evm:1, the pool index eth, the DEX index ethereum). GET /api/v2/chains lists them all. **Response:** NOT the provider envelope — this verb answers from the in-process catalog, so there is no `data`, `source`, `freshness` or `degraded` block. The body is `{success, chain, measuredAt, tiers}`: `chain` is the full catalog row (key, kind, chainId, caip2, displayName, native asset, addressFormat, explorer templates, aliases, providers, tier, tierName, capabilities and the prober evidence), `measuredAt` is when that evidence was taken, and `tiers` publishes the ladder verbatim. For the same row without a key, use GET /api/v2/chains/{chain}. > The authed, uncached twin of the public GET /api/v2/chains/{chain}. ## Chains (catalog) The public chain catalog. 118 chains are recognised (106 EVM, 12 non-EVM); 74 carry live market data, 29 add holder and security analysis, and 59 can be quoted (measured 2026-10-04T04:17:00Z). Tiers are MEASURED every six hours, never declared: L0 recognised — the chain is in the catalog seed: identifiers, native asset and explorer are known; L1 priced — a fresh USD price for a known token on this chain — the newest trade observed is under 24 h old; L2 market — a live tape: the newest trade is under 1 h old, and the chain reports 24 h trade activity or a pool that traded within the hour; L3 forensics — tier 2, and a holder-distribution and token-security analysis completed for the probe token; L4 tradeable — tier 1 or better, and a non-custodial swap route can be quoted — through the aggregator layer or a first-party trade service configured for this chain; L5 services — a first-party trade service is configured for this chain and a paid product on it is live. These endpoints need no API key — they are what an integrator reads before deciding whether to integrate. ### GET /api/v2/chains **List chains (public catalog)** — Every chain in the catalog with its measured tier (0=recognised, 1=priced, 2=market, 3=forensics, 4=tradeable, 5=services), per-capability booleans, the provider NAMES that cover it, address format, explorer templates and aliases. Filters are ANDed. No API key required. `GET https://api.stryke.gg/api/v2/chains` · public **Query parameters:** - `tier` (integer, optional) — EXACTLY this tier, 0-5 — not a minimum. The ladder is a class, not a rank: tier 4 (quotable) does not imply tier 2 (live tape), because a chain can be quotable through an aggregator without our having a live tape for it. To ask "which chains can do X", filter on capability instead. - `kind` (string, optional) — Exact chain kind (evm, svm, move, ton, tron,...). 400 lists the kinds currently present when the value is unknown. - `capability` (string, optional) — One of price, market, ohlcv, trades, pools, holders, security, metadata, portfolio, walletTrades, quote, rpc, services. Only rows where that capability is true. - `q` (string, optional) — Case-insensitive substring match on key, displayName and aliases. At most 64 characters. - `measured` (integer, optional) — 1 = only rows the prober has measured; 0 = only rows it has not. Absent = all. **Response:** count = rows after filtering; total = rows in the catalog; byTier is a histogram of the FILTERED rows; measuredAt is the generated evidence timestamp (ms) or null before the prober has run; tiers publishes the ladder verbatim from the catalog; chains[] are the same public chain rows /chains/{chain} returns, sorted by key. capabilities.* are what routes gate on; tier is informational. measured:false on a row means the capabilities are declared from the seed and nobody has measured them yet. > Assembled per request from one prerendered copy of the catalog, which is rebuilt only when the catalog is (no filtered body ever enters the shared response cache — the key space is finite but each body is up to 273 KB). Strong ETag (sha1 of the body); If-None-Match answers 304. Cache-Control: public, max-age=60. Mounted before validateApiKey; rate-limited per client IP. Never includes rpc URLs, env names or provider-internal ids. ### GET /api/v2/chains/summary **Catalog summary** — Counts only: rows by tier, by kind and by capability, how many rows the prober has measured, how many are in the execution registry, plus the tier ladder and the evidence timestamp. `GET https://api.stryke.gg/api/v2/chains/summary` · public **Response:** generated:false and measuredAt:null together mean the prober has not run yet; every row is then tier 0 with seed-declared capabilities. capabilities. counts rows where that capability is true. > Memoised 60 s. ETag/304 and Cache-Control as for the list. ### GET /api/v2/chains/resolve **Resolve a chain identifier** — Turns any chain identifier (key, alias, display name, EIP-155 id, evm:, eip155:, CAIP-2, or a provider's chain id) into the one catalog row it names, or 404. `GET https://api.stryke.gg/api/v2/chains/resolve` · public **Query parameters:** - `q` (string, required) — The identifier to resolve. Case-insensitive; at most 128 characters. **Response:** chain is the same publicView row the list returns. On a miss: 404 { success:false, error:"unknown chain", hint:"GET /api/v2/chains", suggestions?:[keys] } — suggestions are catalog keys whose key, name or alias contains the query. > Resolution is a single index lookup; ambiguity is impossible by construction (the catalog refuses to load if two rows claim one token). ### GET /api/v2/chains/{chain} **One chain, with the routes it supports** — The publicView row for one chain plus the /api/v2/chain/{chain}/* routes that will work on it given its measured capabilities, and — separately — the ones that will 501, each with the reason. `GET https://api.stryke.gg/api/v2/chains/{chain}` · public **Path parameters:** - `chain` (string, required) — Any identifier the catalog resolves, case-insensitively: the Stryke key (eth), an alias (ethereum), the display name, an EIP-155 id (1), evm:1, eip155:1, any CAIP-2 id, or the id a provider uses for the chain (the market-data layer evm:1, the pool index eth, the DEX index ethereum). **Response:** endpoints[] are available now (authRequired:true — they live under the keyed /api/v2/chain namespace); unavailable[] carries the missing capability and a reason. The capability per route is routing.capOf(verb), the same table the dataplane 501-gates on, so this list and the 501s cannot disagree. /gas is evm-only; /tx is evm and svm; /capabilities is always available. chain.evidence.held, when present, means this probe measured a capability as LOST and the previous verdict is being published for one more run — evidence.held.lost names them and evidence.held.measuredTier is what this run actually measured. A second consecutive miss demotes. A capability GAINED is never held; it is published on the first run that proves it. > Memoised 60 s per resolved key. The authed twin GET /api/v2/chain/{chain}/capabilities returns the same row uncached. ## Screener (pairs) Every DEX pair the index follows, ranked. Price, 5m/1h/6h/24h change, volume, transactions, liquidity, FDV and age — plus the pair's own candles and tape. Served from Stryke's first-party index; no third-party price feed is involved. Ranked lists require a pool whose depth was actually measured, because volume from an unmeasured pool is not trustworthy — see minLiquidityUsd and the volumeTrusted flag on every row. ### GET /api/v2/pairs **Rank pairs** — The screener. Ranked pools with price, change, volume, transactions, liquidity, FDV and age. `GET https://api.stryke.gg/api/v2/pairs` · public **Query parameters:** - `chain` (string, optional) — eth | base | sol, or omit for all three. - `sort` (string, optional) — volume24h (default), volume6h, volume1h, volume5m, liquidity, txns24h, change24h, change6h, change1h, change5m, fdv, age, lastTrade. - `order` (string, optional) — 'desc' (default) or 'asc'. - `minLiquidityUsd` (number, optional) — Defaults to 1. Pass 0 to include pools whose depth was never measured — their volume is NOT trustworthy; see volumeTrusted on each row. - `minVolume24hUsd` (number, optional) — Floor on 24h volume. - `minTxns24h` (number, optional) — Floor on 24h transaction count. - `maxAge` (string, optional) — Only pairs first traded within this long, e.g. '15m', '6h', '7d'. Implies an exact first-trade timestamp. - `minAge` (string, optional) — Only pairs older than this. - `dex` (string, optional) — Exact venue match, e.g. pumpswap, orca-whirlpool, uniswap-v3. - `activeWithin` (string, optional) — Only pairs whose LAST trade is this recent, e.g. '15m', '1h'. Sorted by 24h volume, a pool that pumped and stopped still ranks — one real example carried $164.32M across 167,749 transactions and had been dead for nine hours. Not applied by default. - `token` (string, optional) — Every pair whose BASE side is this token. - `pools` (string, optional) — Comma-separated pool addresses (max 200) — how a watchlist is fetched in one request. - `limit` (number, optional) — 1-500, default 50. - `offset` (number, optional) — 0-10000, default 0. ### GET /api/v2/pairs/trending **Trending pairs** — Ranked by 1-hour volume, with a floor on transaction count so one large trade cannot buy the top of the list. `GET https://api.stryke.gg/api/v2/pairs/trending` · public ### GET /api/v2/pairs/new **New pairs** — Pairs whose FIRST trade the index actually witnessed. Pairs whose history merely begins at the edge of the tape's retention window are excluded rather than shown as new. `GET https://api.stryke.gg/api/v2/pairs/new` · public ### GET /api/v2/pairs/gainers **Gainers** — Ranked by 24h change, with liquidity and transaction floors so the list is not a directory of empty pools, and a recency floor so it is not a directory of pairs that stopped trading. `GET https://api.stryke.gg/api/v2/pairs/gainers` · public **Query parameters:** - `activeWithin` (string, optional) — How recently the pair must have traded, e.g. '15m', '2h'. Defaults to 2h on this feed; '0' removes it. - `minLiquidityUsd` (number, optional) — Defaults to 5000 here. - `minTxns24h` (number, optional) — Defaults to 50 here. - `chain` (string, optional) — eth | base | sol, or omit for all three. - `limit` (number, optional) — 1-500, default 50. **Response:** Two recency rules apply. A change is never ranked for a pair that did not trade inside that window — without it this feed led with a pair at +4,959,000,000,000,000% that had zero candles in 24 hours. On top of that both mover feeds default to pairs that traded in the last 2 hours, because a “biggest loser” nobody can trade is a museum exhibit: the top of /losers was six rows at -100% whose last trades were 8 to 23 hours earlier. Pass activeWithin=24h to widen it, or activeWithin=0 to remove it; filters echoes what applied. ### GET /api/v2/pairs/losers **Losers** — Ranked by 24h change ascending, same floors as gainers. `GET https://api.stryke.gg/api/v2/pairs/losers` · public **Query parameters:** - `activeWithin` (string, optional) — How recently the pair must have traded, e.g. '15m', '2h'. Defaults to 2h on this feed; '0' removes it. - `minLiquidityUsd` (number, optional) — Defaults to 5000 here. - `minTxns24h` (number, optional) — Defaults to 50 here. - `chain` (string, optional) — eth | base | sol, or omit for all three. - `limit` (number, optional) — 1-500, default 50. **Response:** Two recency rules apply. A change is never ranked for a pair that did not trade inside that window — without it this feed led with a pair at +4,959,000,000,000,000% that had zero candles in 24 hours. On top of that both mover feeds default to pairs that traded in the last 2 hours, because a “biggest loser” nobody can trade is a museum exhibit: the top of /losers was six rows at -100% whose last trades were 8 to 23 hours earlier. Pass activeWithin=24h to widen it, or activeWithin=0 to remove it; filters echoes what applied. ### GET /api/v2/pairs/search **Search pairs** — By token symbol (prefix), token name (contains), or exact token/pool address. Ordered by liquidity. `GET https://api.stryke.gg/api/v2/pairs/search` · public **Query parameters:** - `q` (string, required) — Symbol, name or address. ### GET /api/v2/pairs/stats **Index totals** — Per-chain pair counts and 24h volume, published both filtered and unfiltered so the difference stays visible. `GET https://api.stryke.gg/api/v2/pairs/stats` · public ### GET /api/v2/pairs/{chain}/{pool} **One pair** — A pair with both tokens hydrated, Solana mint/freeze authority where it applies, and the other pools trading the same token. `GET https://api.stryke.gg/api/v2/pairs/{chain}/{pool}` · public ### GET /api/v2/pairs/{chain}/{pool}/candles **Pair candles** — OHLCV for the pair, on the base side. Intervals 60, 300, 900, 3600, 14400, 86400. `GET https://api.stryke.gg/api/v2/pairs/{chain}/{pool}/candles` · public **Query parameters:** - `interval` (number, optional) — 60, 300, 900, 3600, 14400 or 86400 seconds. Default 300. - `limit` (number, optional) — 1-1000, default 300. - `from` (string, optional) — ISO timestamp or epoch ms. WITH from, the response is the window STARTING there. Without it, the response is the most recent `limit` bars. - `to` (string, optional) — ISO timestamp or epoch ms. **Response:** Always ascending by bucketTs. With no `from`, this returns the LATEST `limit` bars — a chart means the most recent ones, and an ascending read with a limit would otherwise return the oldest (measured: base’s busiest pool at interval=60 came back six days stale). vUsd is NULL, not 0, when no trade in the bucket could be valued. buys and sells are NULL for buckets folded before the split was counted. ### GET /api/v2/pairs/{chain}/{pool}/traders **Top traders** — Which wallets are trading this pool, with their trade counts and buy/sell split — the signal that separates an organic market from a handful of bots. `GET https://api.stryke.gg/api/v2/pairs/{chain}/{pool}/traders` · public **Query parameters:** - `minutes` (number, optional) — Window, 1-360, default 60. Bounded because this groups the raw tape by maker: one hour costs 172ms on Solana’s busiest pool and 24 hours costs 26.7 seconds. - `limit` (number, optional) — 1-100, default 25. ### GET /api/v2/pairs/{chain}/{pool}/trades **Pair trades** — The pair's own tape, newest first, from the index's raw swaps. `GET https://api.stryke.gg/api/v2/pairs/{chain}/{pool}/trades` · public ## Wallets (first-party) What an address has actually traded, read from Stryke’s own tape. Positions, current value and average-cost PnL across Ethereum, Base and Solana. Read-only: nothing here holds a key, signs, or moves a balance. Every figure is window-scoped — the raw tape retains 14 days — and each row says whether its position is complete within the window and what share of its trades were priced. ### GET /api/v2/wallet/{chain}/{address} **Wallet positions** — Every token an address has traded, with net position, current value and average-cost PnL — from Stryke's own tape, not a vendor. `GET https://api.stryke.gg/api/v2/wallet/{chain}/{address}` · public **Path parameters:** - `chain` (string, required) — eth | base | sol. - `address` (string, required) — The wallet, in the chain's own address format. **Query parameters:** - `hours` (number, optional) — Window to look back over, default 168 (7 days). 0 means the whole retained tape (14 days) and is slower on high-frequency wallets. **Response:** Every figure is WINDOW-SCOPED. A position whose first trade predates the window has positionComplete:false and an understated cost basis. usdCoverage says what fraction of the trades the follower priced — 0 on eth and base, where no USD is written to the tape. ### GET /api/v2/wallet/{chain}/{address}/balances **Wallet balances** — What the address actually HOLDS, read from the chain through Stryke’s own RPC — not what it traded. `GET https://api.stryke.gg/api/v2/wallet/{chain}/{address}/balances` · public **Path parameters:** - `chain` (string, required) — eth | base | sol. - `address` (string, required) — The wallet, in the chain's own address format. **Query parameters:** - `limit` (number, optional) — Holdings to return, 1-500, default 200. Sorted by value. **Response:** Every holding is returned and each carries `dust` (priced below dustBelowUsd) so a client can filter and say how many it hid — a balance the chain reports is never silently discarded, and an UNPRICED holding is not dust but unknown. coverage.complete says whether every held token could be listed. On Solana it is true — getTokenAccountsByOwner enumerates, and accounts are summed per mint. On EVM it is FALSE by construction: there is no way to list an address’s ERC-20s, so the candidate set is the tokens the index has seen it trade and an untraded airdrop will not appear. A holding whose price is not anchored carries priceUsd null rather than a price the feedback loop may have manufactured, so totalValueUsd is a floor rather than an estimate. ### GET /api/v2/wallet/{chain}/{address}/trades **Wallet trades** — The address's own tape, newest first. `GET https://api.stryke.gg/api/v2/wallet/{chain}/{address}/trades` · public **Query parameters:** - `limit` (number, optional) — 1-200, default 50. - `hours` (number, optional) — Look-back window, default 168. - `side` (string, optional) — 'buy' or 'sell'. ## Tokens (first-party) One token across every market it trades in — identity, the price its deepest market sets, cross-pool totals and the market list. Read from Stryke’s own index on Ethereum, Base and Solana; no third-party price feed is involved. The quoted price comes from the deepest pool whose depth was actually MEASURED, and is null when there is none — a price from an unmeasured pool is the exact failure the liquidity floor exists to stop. Read-only: nothing here holds a key, signs, or moves a balance. ### GET /api/v2/token/{chain}/{address} **Token overview** — One token: identity, the price its deepest market sets, cross-pool totals, and every market it trades in. `GET https://api.stryke.gg/api/v2/token/{chain}/{address}` · public **Path parameters:** - `chain` (string, required) — eth | base | sol. - `address` (string, required) — The token, in the chain's own address format. **Query parameters:** - `limit` (number, optional) — Markets to return, 1-200, default 50. **Response:** price.source says which rule produced the number: 'anchored-token-price' is the token's own cross-pool price, published only when it was reached in one hop from a stablecoin or the wrapped native AND that hop crossed a pool with measured depth, and only when it is under 15 minutes old; 'deepest-measured-pool' is the fallback. price is NULL unless one of those holds — a price from a pool with no MEASURED depth — a price from an unmeasured pool is the exact failure mode the liquidity floor exists to stop. market.liquidityUsd and market.volume24hUsd describe the markets containing the token, counting both sides of each pool; each pool is counted once. authorities is present only on Solana, where a null authority means renounced. ### GET /api/v2/token/{chain}/{address}/pools **Token markets** — Every pool the token trades in, deepest first, with the side it sits on. `GET https://api.stryke.gg/api/v2/token/{chain}/{address}/pools` · public **Query parameters:** - `limit` (number, optional) — 1-500, default 100. - `minLiquidityUsd` (number, optional) — Default 0 — a token page must not hide a token's only market. Rows carry liquidityUsd and volTrusted so the caller can mark them. ### GET /api/v2/token/{chain}/{address}/candles **Token OHLCV** — The token's cross-pool candle series — each bucket from that bucket's deepest eligible pool, never averaged. `GET https://api.stryke.gg/api/v2/token/{chain}/{address}/candles` · public **Query parameters:** - `interval` (number, optional) — 60, 300, 900, 3600, 14400 or 86400 seconds. Default 300. - `limit` (number, optional) — 1-1000, default 300. - `from` (string, optional) — ISO timestamp or epoch ms. - `to` (string, optional) — ISO timestamp or epoch ms. **Response:** buys and sells are NULL, not 0, for buckets folded before the builder counted the split. vUsd is NULL when no trade in the bucket could be valued. ### GET /api/v2/token/{chain}/{address}/trades **Token tape** — Every trade of one token across all its markets, newest first — not one pool's tape. `GET https://api.stryke.gg/api/v2/token/{chain}/{address}/trades` · public **Query parameters:** - `limit` (number, optional) — 1-200, default 50. - `hours` (number, optional) — Look-back window, default 24. The raw tape retains 14 days. - `side` (string, optional) — 'buy' or 'sell', oriented to this token. - `minUsd` (number, optional) — Floor on the trade value in dollars. On a chain whose follower prices its own swaps this is pushed into the query; where it does not (eth and base write no USD at all) the tape is valued at read time and filtered here, and `minUsdApplied` says which happened. When it is 'after-valuation', `scanned` says how many recent trades were examined — fewer results than `limit` at the scan cap means the window may hold more, further back. **Response:** usdCoverage says what fraction of the rows carry a dollar value. The EVM followers write NO USD to the tape (measured 0% on eth and base), so every figure there is derived at read time from token prices and each row's usdBasis says which: 'follower', 'derived' or 'unpriceable'.