Data API
A public, read-only analytics API over The Syndicate’s live game state: the capo roster, supply, marketplace listings, on-chain sale history, the RACKET economy, leaderboards, season standings, territory control, combat, bounties, the trainer- and boost-jobs boards, prestige-recipe usage, per-capo RACKET production, and per-player SOL trading. It is cache-backed and stable — built for dashboards, explorers, and analytics sites (it powers thesyndicate.fyi). All responses are scrubbed of personal data.
Authentication
Every request requires a server-side API key, sent as the x-api-key header. Keys are issued per consumer (they look like syn_live_…), carry their own rate-limit budget, and may be revoked individually. Keep your key server-side — never ship it to a browser.
curl -s https://thesyndicate.games/api/v1/supply \ -H "x-api-key: $SYNDICATE_API_KEY"
To request a key, email tim@thesyndicate.games.
Conventions
- Base URL:
https://thesyndicate.games/api/v1 - Envelope: every response is
{ "as_of": "<ISO-8601 UTC>", "data": … }so you can render “updated Xs ago.” - Units are in the field name:
*_lamports= SOL base units (÷1e9),*_racket= whole RACKET (integer, no decimals),*_usd= US dollars. - Timestamps: UTC ISO-8601.
- Caching: responses set
Cache-Control: private, max-age=<n>matching each endpoint’s change-cadence. It’s a client-side freshness hint — responses are deliberately kept out of shared CDN caches so the API key is re-checked on every request. Poll on the cadence and cache on your side. - Conditional requests (recommended for pollers): every response carries a weak
ETagcomputed overdata(theas_ofstamp is excluded, so the tag only changes when the data does). Echo it back viaIf-None-Matchand you’ll get an empty304 Not Modifiedwhen nothing changed — the big feeds are hundreds of KB, so a polling bot that sends the header skips almost all of that transfer between refreshes:ETAG=$(curl -sI https://thesyndicate.games/api/v1/capos \ -H "x-api-key: $SYNDICATE_API_KEY" | awk 'tolower($1)=="etag:"{print $2}') curl -s https://thesyndicate.games/api/v1/capos \ -H "x-api-key: $SYNDICATE_API_KEY" -H "If-None-Match: $ETAG" \ -o /dev/null -w "%{http_code}\n" # 304 until the snapshot refreshes - Polling & change-cadence: each endpoint notes a change-cadence (e.g. ~60s, ~hourly) — that’s how often the underlying data actually changes, so polling faster just returns the same payload. Two shapes:
- Append-only feeds (
/v1/combat/fights,/v1/market/sales,/v1/bounties,/v1/contracts/open-jobs,/v1/boosts,/v1/royalties) — incremental. Pass?since=<the newest timestamp you’ve seen>to fetch only new rows instead of the whole history. - Full snapshots (
/v1/territory,/v1/capos,/v1/supply,/v1/contracts, etc.) — every call returns the complete current state, with no?since. To track changes, diff two consecutive snapshots (e.g. oncontroller_ref/last_contested_atfor territory).
- Append-only feeds (
- Rate limit: read-tier (240 requests/min, per key). Exceeding it returns
429.
Privacy
These endpoints never expose personal data — no email, Telegram, Discord, Privy, IP, or internal identifiers. Players appear only as a public display_name plus an opaque, non-reversible owner_ref / seller_ref / player_ref for grouping (the same hash across endpoints, so it joins). Wallet addresses appear only as on-chain marketplace counterparties on completed sales (which are already public on Solana).
Endpoints
The full live capo roster (~3–4k rows; Common/Uncommon have a null mint_address). Powers supply, stat distributions, leaderboards, and market enrichment. Change-cadence: ~hourly.
Each capo includes: capo_id, mint_index, mint_address, rarity, is_founder, tier, tier_promoted_at, role, specialty, personality, character_name, name, gender, the three traits (trait_greed/finesse/passion), leash_level, total_racket_invested, status, season_created, created_at, stats_updated_at, owner_ref, and owner_display_name. (Combat stats are not on the roster — the marketplace is the only surface that exposes them; see /v1/market/listings.)
Per-capo RACKET production from side-hustles — how much $R each capo earns, highest lifetime first. One row per capo that has earned. RACKET is in-game currency (a gameplay stat). Served from an hourly snapshot. ~15 min.
Each capo: capo_id, capo_name, capo_rarity, capo_tier, owner_ref / owner_display_name, total_racket_earned (lifetime), active_days (distinct UTC days it earned), avg_racket_per_day (total ÷ active_days), racket_current_season, racket_last_7d / racket_last_30d, first_earned_at / last_earned_at. All amounts are whole RACKET.
Params: ?capo=<uuid> (one capo), ?owner=<player_ref> (all of a player’s capos), ?min_earned= (floor on lifetime total). A historical per-season breakdown isn’t offered — the season windows overlap in the source data, so only the clean current-season total is exposed.
Capo supply: live circulating counts, burned (salvaged) counts, and the full daily supply curve since launch. Change-cadence: ~hourly.
Live circulating — per_rarity { common, uncommon, rare, epic, legendary, god, founder } (founder is its own rarity, so it’s one of the seven keys), founder (founder count — also included in per_rarity and total), and total.
Burned — burned_per_rarity (same seven rarity keys, zero-filled) and burned_total: capos destroyed by salvage or prestige sacrifice, which the live counts exclude.
History — history is one point per UTC day, oldest → newest, each { date, total, per_rarity, burned_total, burned_per_rarity }. total / per_rarity are circulating supply at the end of that day (minted on/before it, minus burned on/before it); burned_total / burned_per_rarity are the cumulative burned as of that day (mirroring the top-level burned fields, which equal the newest point). Roughly 50 points today, growing by one per day.
The full live crafted-item roster (~2.3k). item_type is the item name (e.g. fedora); mint_address is the Bubblegum cNFT mint. Fields: item_id, item_type, slot, rarity, durability / max_durability, primary_stat / primary_stat_pct, secondary_stat / secondary_stat_pct, listed_on_marketplace, crafted_at, mint_address. Full snapshot; ~hourly.
This endpoint deliberately does not expose an item’s owner or the capo it’s equipped to — a capo’s equipped loadout is its hidden combat buffs, so it’s kept private (same reason exact rival stats aren’t public). For global equipped / available counts, use /v1/equipment/supply (unattributed aggregates).
primary_stat_pct / secondary_stat_pct are the item’s live stat boost as a percentage of the equipped capo’s stat (rarity-derived: primary +5 / 10 / 15 / 20%, secondary half that). They replace the old flat *_stat_bonus point fields, which were retired — equipment bonuses are percentages, not flat points.
The response also carries the deflationary sink: burned_merged: { total, by_type: [{ item_type, count }] } — the destroyed (burned + merged) items the live roster omits. Burn vs merge isn’t split (both soft-delete).
Crafted-item status breakdown, including DESTROYED items the live roster omits. live: { total, equipped, broken, per_type, per_rarity }; destroyed: { total, per_type, per_rarity }. Note: burn vs merge isn’t split — both destruction paths just soft-delete, so “destroyed” is a single bucket.
All active listings — both in-game (RACKET) and Tensor (SOL) — tagged by source (in_game | tensor). Full snapshot. Change-cadence: ~60s.
Fields: listing_id, capo_id, item_id, listing_type, nft_mint_address, source, price_lamports, price_racket, listed_at, seller_ref, seller_display_name, tensor_list_state_pda, tensor_list_tx_signature, promo_next_tier, completed_achievements.
Capo listings additionally carry the listed capo’s identity, class, and combat stats (all null for equipment listings): name, character_name, age, rarity, tier, the three traits (trait_greed / trait_finesse / trait_passion), stat_muscle / stat_hustle / stat_brains / stat_rep / stat_grit, stat_budget_used. The marketplace is the only surface that exposes capo stats. age matches the in-app marketplace’s derived age (founders age via founder aging; others = 25 + seasons aged, minus any elixir rewind). name is the current (possibly renamed) display name; character_name is the capo’s original character name.
Promotion (capo listings; null / [] for equipment): promo_next_tier is the rank the capo is ready to promote to — it has completed its current tier's promotion achievement and age requirements — else null. completed_achievements is the list of promotion achievements it has completed (complete_side_hustle, attempt_takeovers_5, win_passion, hold_district_1d, reach_tournament_semifinal). The win_passion promotion gate accepts 1 Passion flip or 25 initiated battles. Legacy boost achievements may also appear.
Query params (optional; repeatable or comma-separated; invalid values ignored): ?promo_next=lieutenant,underboss (only listings ready to promote to any of these ranks) and ?achievement=win_passion (only listings whose capo has completed all the listed achievements).
Deep links. Link straight to a listing’s page on the site from its row. Capos use https://thesyndicate.games/marketplace?listing=<nft_mint_address>; equipment uses https://thesyndicate.games/marketplace?equipment=<listing_id>. Note the params differ — capos key on the mint, equipment on the listing_id — and each opens that listing’s buy sheet directly.
curl -s "https://thesyndicate.games/api/v1/market/listings?promo_next=lieutenant&achievement=win_passion" \ -H "x-api-key: $SYNDICATE_API_KEY"
Consolidated sale history (Tensor + in-game), newest first. Append-only. Change-cadence: ~60s.
Query params:
?limit=— page size, default 1000, max 5000. (The full 5000 is returned even though the underlying store caps a single read at 1000 — it’s paged internally.)?cursor=— opaque token from the previous response’snext_cursor; returns the next older page. Keyset-based, so it’s stable across the timestamp ties that batch settlements create. To pull the full history, start with no cursor and follownext_cursoruntil it isnull.?since=<ISO-8601>— lower time bound (only sales at/after this time), for forward incremental polling. Composes with?cursorto drain everything since a given time.?asset_kind=— filter by asset kind (capo|founder|equipment; repeatable or comma-separated). Server-side, so a capo-only consumer needn’t pull the whole feed. Founders are their own kind, so all capo sales are?asset_kind=capo,founder.
The response adds next_cursor (the token for the next page, or null at the end) alongside count, limit, and since.
Consolidates Tensor sales and in-app v4 settled sales, deduped. Fields: sale_id, signature, mint_address, capo_id, item_id, asset_kind (capo | founder | equipment), price_lamports, price_racket, usd_price_locked, treasury_fee_usd, source (tensor | in_game), sold_at. Counterparties: Tensor sales carry on-chain buyer_wallet / seller_wallet; in-app sales carry buyer_display_name / seller_display_name instead. Both also carry buyer_ref / seller_ref — the opaque player_ref for each side (resolved from the wallet on Tensor / player_id in-game; null when the counterparty isn’t a known player). Join them to /v1/market/traders.
curl -s "https://thesyndicate.games/api/v1/market/sales?since=2026-06-01T00:00:00Z&limit=100" \ -H "x-api-key: $SYNDICATE_API_KEY"
Per-player SOL trading on the secondary market — one row per player who has bought or sold for SOL, highest net first. Attributed via the on-chain wallet → player mapping (Tensor) and player_id (in-game), so it’s complete wherever the counterparty is a known player. Covers marketplace sales only (swaps excluded). ~5 min.
Each trader: player_ref (joins the other per-player feeds), display_name, sales_sold / sol_sold_lamports (as seller), sales_bought / sol_bought_lamports (as buyer), and net_sol_lamports (sol_sold − sol_bought, the realized SOL). Amounts are lamports.
RACKET economy snapshot. totals: supply + lifetime earned/spent + market revenue/spend (all *_racket, whole RACKET). daily_emissions: per-day { day, minted, burned, net }. sink_distribution: { type, total }. ?days (default 30, max 365) sets the emissions/sink window.
PII-free leaderboards. earnings (top capos by RACKET earned) and combat_wins (by net takeover victories), each { capo_id, capo_name, tier, owner_display_name, …, rank }. ?season= (int, default 0 = current/all).
Per-player territory-combat records (settled fights only), one row per player — leaderboard-ready. Full snapshot; ~5 min. Each row: player_ref, display_name, attacks_total/won/lost, attacks_won_by_passion / attacks_lost_to_passion, attacks_extorted, defenses_total/held/lost, defenses_held_by_passion / defenses_lost_to_passion, fights_total/won, passion_flips_for/against, distinct_opponents, districts_contested, first_fight_at / last_fight_at.
Params: ?ref=<player_ref> (single player) and ?season=<n> (scope to one season; rows then carry a season field). Without ?season the totals are lifetime. Response: { count, season, players }.
Per-fight territory-combat feed (settled fights only — in-flight takeovers are never exposed), newest first. Append-only — poll incrementally with ?since. Change-cadence: ~60s.
Each fight carries both sides (attacker_ / defender_ ref, name, capo, specialty), district / city, outcome, winner, passion_flip, extorted, and resolved_at. (The raw fight scores and matchup odds are intentionally not exposed — they reveal capo combat strength.)
Params: ?since=<ISO> (resolved_at at/after), ?limit= (default 500, max 2000), ?ref=<player_ref> (fights where the player was attacker or defender), and ?season=<n> (fights initiated that season; rows carry season).
Who attacked whom: one row per season, city and attacker/defender pair, over settled fights. In-flight and cancelled takeovers, fights on unclaimed hexes and self-attacks are never included. Built for attacker-vs-defender matrices. Snapshot; ~15 min.
Each row: season, city_id, city_name, league, attacker_ref / attacker_name, defender_ref / defender_name, attempts, taken (attacker won, extortions included), held (defender won), extorted, passion_flips, first_at / last_at. Names are current display names.
Params: ?season=<n> (season the fights were started in), ?league=, ?city=<uuid>, ?ref=<player_ref> (rows where the player is attacker or defender), ?limit= (default and max 5000) and ?cursor=. Follow next_cursor until it’s null to read everything; rows come in a fixed key order, so a walk never skips or repeats a row. Response: { count, limit, next_cursor, current_season, refreshed_at, matchups }.
curl -s "https://thesyndicate.games/api/v1/combat/matchups?season=18&league=kingpin" \ -H "x-api-key: $SYNDICATE_API_KEY"
Season standings: one row per season and player with player_ref, display_name, league, city_id / city_name, season heat, league_rank, city_rank and promotion_result. For the current season league_rank is the live rank, in the in-game league board’s order; for past seasons it’s the final rank (null for players archived as inactive). Players who moved city mid-season appear once, in the city they finished in.
Params: ?season=<n> (default: current season), ?league=, ?city=<uuid>. Response: { season, current_season, final, count, standings }. Full snapshot; ~5 min.
The live district map + current control — one row per district (~2k; ~900 occupied). The stable district_id / city_id are your tracking keys. Poll and diff on occupied_since, last_contested_at, and controller_ref to detect control flips; the per-flip detail (who attacked whom) is in /v1/combat/fights, which carries the same district_id. Full snapshot; ~5 min.
Each district: district_id, city_id, city_name, city_league, district_name, resource_type, value_tier, heat_bonus, hex_q / hex_r (axial coords), is_occupied, occupied_since, last_contested_at, claimed_this_season. When occupied, the controller (else null): controller_ref, controller_display_name, controller_capo_id / controller_capo_name / controller_capo_rarity, assignment_type (claim | takeover_win | admin), controlled_since. No capo stats are exposed.
Params: ?city=<uuid>, ?occupied=true|false.
One row per active city — the map rollup companion to /v1/territory (join on city_id). Fields: city_id, city_name, league, season_number, season_started_at / season_ends_at, max_players, current_player_count, total_districts, emission_pool_multiplier, is_active, districts_total, districts_occupied. ~5 min.
RACKET + SOL player bounties, unified, newest first. Every bounty targets a player (target_ref); target_district_id scopes it to a district when set. kind is racket | sol. Change-cadence: ~5 min.
Fields: bounty_id, kind, city_id / city_name, target_district_id, season, amount_racket / collector_payout_racket (whole RACKET), pool_lamports / payout_lamports / fee_lamports (SOL bounties), poster_ref / target_ref / collector_ref (+ matching _display_name), collected, collected_at, refunded, is_system_seeded, created_at. System-seeded bounties have a null poster.
Params: ?status=open|collected|all (default all), ?kind=racket|sol, ?city=<uuid>, ?since=<ISO> (created_at at/after), ?limit= (default 500, max 2000).
The supply side of the trainer marketplace (in-app “Contracts”): trainers listing themselves. One row per active trainer listing — a standing rate card an owner can hire off to have that trainer spend their daily upgrade slots on the owner’s capo. For the demand side (jobs owners have posted for trainers to claim), see /v1/contracts/open-jobs below. Full snapshot; ~5 min.
Each listing: listing_id, rate_lamports (SOL per upgrade), allowed_stats (which stats the trainer will train; null = any of muscle/hustle/brains/rep/grit), min_actions / max_actions, max_concurrent_fills, active_fills, available_slots, is_available, created_at / updated_at. The trainer: trainer_ref, trainer_display_name, trainer_prestige_level, trainer_season_pass_tier, and their reputation rollup — trainer_jobs_settled / trainer_jobs_completed, trainer_completion_rate, trainer_on_time_rate, trainer_avg_turnaround_hours (rates are 0–100, null until the trainer has a settled sample). No capo stats or escrow details are exposed.
Sorted cheapest rate first. Params: ?available=true|false (open capacity), ?stat=<muscle|hustle|brains|rep|grit> (listings that train that stat; null-allowed_stats listings match any).
The demand side: the Open Jobs board. One row per live, unclaimed job an owner has posted for any eligible trainer to claim (the complement to /v1/contracts, where trainers list themselves). Newest first — poll with ?since=<newest created_at you’ve seen> to surface just the newly-opened jobs (for “new job” alerts). ~2 min.
Each job: job_id, owner_ref / owner_display_name, the capo to train — capo_id / capo_name / capo_rarity (no stats), allowed_stats (which stats; null = trainer’s choice), total_actions (upgrades to deliver), consideration_lamports (SOL pay; 0 for pure barter), house_fee_lamports, is_barter / payment_capo_id / payment_capo_rarity (a capo offered as payment), min_completion_rate (0–100 proven- completion bar to claim; null = none), deadline, created_at.
Params: ?since=<ISO> (created_at at/after), ?stat=<muscle|hustle|brains|rep|grit> (jobs requesting that stat; null-allowed_stats jobs match any), ?limit= (default 500, max 2000).
Boost Contracts are closed to new bookings. This endpoint returns boost_jobs: [], count: 0, and retired: true. Existing contracts continue with their original goals and payouts.
Prestige analytics — all aggregate (no player identity), safe to render publicly. ~5 min. Four blocks in data:
recipes — one row per distinct recipe. A recipe is “sacrifice count capos of a minimum rarity, each meeting a per-capo stat threshold” to reach prestige to_level. Fields: to_level, rarity, count (capos per use), stat (per-capo threshold), times_used (all-time), times_used_7d / times_used_30d (rolling, for trends), last_used.
levels — scarcity per rank: { level, holders, holders_pct }. holders is the number of players currently AT that prestige level; holders_pct is that as a percentage of all registered players.
totals — { prestiges, capos_sacrificed, players_prestiged }.
daily — per (UTC day, level) counts: { date, to_level, count }, oldest first, for the over-time chart.
On-chain NFT royalty receipts (Tensor secondary sales) with 24h / 7d / all-time SOL volume rollups. ?since=<ISO> and ?limit= (default 500, max 2000). Each receipt: signature, block_time, lamports, asset_kind, mint_address, source.
Example response
GET /v1/supply:
{
"as_of": "2026-06-29T10:18:00.000Z",
"data": {
"per_rarity": { "common": 10374, "uncommon": 4927, "rare": 3113,
"epic": 890, "legendary": 150, "god": 22, "founder": 729 },
"founder": 729,
"total": 20205,
"burned_per_rarity": { "common": 2659, "uncommon": 894, "rare": 368,
"epic": 86, "legendary": 4, "god": 0, "founder": 0 },
"burned_total": 4011,
"history": [
{ "date": "2026-05-13", "total": 1,
"per_rarity": { "common": 0, "uncommon": 0, "rare": 0, "epic": 0,
"legendary": 0, "god": 0, "founder": 1 },
"burned_total": 0,
"burned_per_rarity": { "common": 0, "uncommon": 0, "rare": 0, "epic": 0,
"legendary": 0, "god": 0, "founder": 0 } }
// … one point per UTC day, oldest → newest, up to today
]
}
}Roadmap
Further aggregate surfaces are planned: bucketed game-activity (combat, hustle, crafting), packs, territory, tournaments, achievements, and player summaries. Want one sooner? Let us know.
