Documentation Index
Fetch the complete documentation index at: https://docs.ticktock.bet/llms.txt
Use this file to discover all available pages before exploring further.
Overview
The Ticktock Messaging Feed delivers real-time market updates through AMQP 0-9-1 (RabbitMQ). Messages are lightweight XML payloads containing only changed data — odds, settlements, and fixture changes. The REST API complements the feed for recovery and static data.
| Property | Value |
|---|
| Protocol | AMQP 0-9-1 (RabbitMQ) |
| Payload | XML |
| Exchange | ttfeed (topic) |
| Heartbeat | <alive> every 10 seconds |
| Localisation | English (textEN) + Russian (textRU) |
| Product | 2 (live) |
Architecture
Connection
Each B2B operator gets an isolated RabbitMQ vhost and a dedicated user:
| Property | Value |
|---|
| Server | mq.ticktock.bet |
| Port | 5672 (AMQP) / 5671 (AMQPS) |
| Virtual Host | /<org-slug> (e.g. /acme-sports) |
| Username | op-<org-slug> (e.g. op-acme-sports) |
| Exchange | ttfeed |
amqp://op-<slug>:<password>@mq.ticktock.bet:5672/<slug>
Operators cannot access each other’s queues or messages.
Getting Credentials
Contact your account manager or email b2b@ticktock.bet to request AMQP access provisioning.
Credentials are generated via the admin panel and returned once — store them immediately.
Connection Limits
Each B2B account is provisioned with the following resource limits per environment (Production and Integration are counted separately):
| Resource | Limit | Notes |
|---|
| Connections | 10 | concurrent AMQP connections across all consumers |
| Channels | 200 | total channels across all your connections (= 20 channels per connection on average) |
| Queues | 10 | total queues declared in your vhost |
Use AMQP channels to multiplex multiple consumers/subscriptions over a single TCP connection — channels are cheap (one Erlang process each) and a well-designed integration typically needs one or two connections even at scale. Opening many short-lived connections is an anti-pattern: it strains both client and broker, and you will hit the cap quickly.
Most integrations run comfortably on 1–2 connections with a handful of channels. If your architecture genuinely needs more (for example, you horizontally scale consumers across many Kubernetes pods, each with its own connection), contact your account manager — we can raise these limits per account after a short review.
Routing Keys
Messages use an 8-section topic routing key:
{priority}.{prematch}.{live}.{msg_type}.{sport}.{source}:{entity}.{event_id}.{node_id}
| Section | Description | Example values |
|---|
| priority | Processing priority | hi / lo / - |
| prematch | Pre-match flag | pre / - |
| live | Live flag | live / - |
| msg_type | Message type | odds_change, bet_settlement, alive |
| sport | Sport ID | 3 (CS2) |
| source:entity | Event type URN | od:match |
| event_id | Match number | 2588141 |
| node_id | Recovery node | - |
Examples
| Message | Routing Key |
|---|
| Odds change for match 2588141 | hi.-.live.odds_change.3.od:match.2588141.- |
| Bet settlement for match 2588141 | hi.-.live.bet_settlement.3.od:match.2588141.- |
| Fixture change | hi.-.live.fixture_change.3.od:match.2588141.- |
Recovery odds replay (priority lo, optional node_id) | lo.-.live.odds_change.3.od:match.2588141.nodeA |
Recovery <snapshot_complete> | -.-.-.snapshot_complete.-.-.-.nodeA |
| Alive heartbeat | -.-.-.alive.-.-.-.- |
Binding Patterns
Use wildcards # (multi-section) and * (single section) to filter messages:
# All live messages (odds + settlements + fixture changes)
hi.-.live.#
# Only odds changes
hi.-.live.odds_change.#
# All messages for a specific match
hi.-.live.*.3.od:match.2588141.-
# Everything including alive heartbeats
#
Message Types
| XML Element | Routing Segment | Priority | Description |
|---|
<odds_change> | odds_change | high | New or updated market odds |
<bet_settlement> | bet_settlement | low | Markets settled — grade bets |
<bet_cancel> | bet_cancel | low | Markets voided — return stakes |
<rollback_bet_settlement> | rollback_bet_settlement | low | Previous settlement reversed |
<fixture_change> | fixture_change | high | Match started, cancelled, or finished |
<alive> | alive | - | Heartbeat — every 10 seconds |
<snapshot_complete> | snapshot_complete | - | Initial state delivery complete after subscription |
Match Identification
Every match-scoped message carries two identifiers:
| Attribute | Format | Purpose |
|---|
event_id | od:match:{number} | Oddin match ID — stable external identifier for cross-referencing |
tt_match_id | UUID | Internal match UUID — use for REST API lookups and settlement queries |
Odds Change
Published when markets are created or odds update. The message is a delta — it contains only changed markets. Markets not present in the message remain unchanged.
<odds_change product="2" timestamp="1711234567890"
event_id="od:match:2588141"
tt_match_id="98deb7ef-def9-47d4-854c-a13618952f99">
<odds>
<market id="1001"
tt_market_id="a3f1b2c4-5678-4d9e-b012-abcdef123456"
type="headshot_opening"
specifiers="map=1|round=5"
status="1"
textEN="First kill is a headshot — Round 5"
textRU="Первое убийство хедшотом — Раунд 5">
<outcome id="1" active="1" odds="2.10" probabilities="0.41000" />
</market>
<market id="1013"
tt_market_id="b7e2c3d5-6789-4e0f-c123-bcdef2345678"
type="grenade_kill"
specifiers="map=1|round=5"
status="1"
textEN="Grenade kill in Round 5"
textRU="Убийство гранатой в раунде 5">
<outcome id="1" active="1" odds="7.40" probabilities="0.10000" />
</market>
<market id="1050"
tt_market_id="c8f3d4e6-7890-4f1a-d234-cdef34567890"
type="bomb_planted"
specifiers="map=1|round=5"
status="1"
textEN="Bomb planted — Round 5"
textRU="Бомба заложена — Раунд 5">
<outcome id="1" active="1" odds="1.85" probabilities="0.47000" />
</market>
</odds>
</odds_change>
Key attributes:
| Attribute | Description |
|---|
market.id | Stable integer market type code (1001–1091) |
market.tt_market_id | Unique UUID for this specific market instance — use for bet placement, settlement correlation, and audit |
market.status | Market status code (see table below) |
market.type | Market type identifier (e.g. grenade_kill, headshot_opening) |
market.specifiers | Pipe-separated context keys. Keys: map=N (integer map number), map_name=Mirage (in-game map name, when known), round=N (round number), player=Name (player-scoped markets), competitor=Team (team-scoped markets — eco_upset, eco_frags, force_buy_win). Example: map=1|map_name=Mirage|round=5|player=s1mple. Present on every message type: odds_change, bet_settlement, bet_cancel, rollback. |
market.textEN / market.textRU | Human-readable market description — display directly to end-users |
outcome.active | 1 = active, 0 = deactivated |
outcome.odds | Decimal odds for this outcome |
outcome.probabilities | Implied probability (e.g. 0.41000) |
Primary integration contract: market.id (stable integer type code) + specifiers uniquely identifies a market type within a match context. market.tt_market_id is a unique UUID for each individual market instance — use it for bet placement and settlement correlation. textEN / textRU is the recommended way to display markets. The type and specifiers attributes are optional convenience fields for operators who want to build custom UIs or map markets programmatically. The specifiers attribute always includes map=N and round=N; it adds map_name=Mirage when the map name is known, player=Name for player-scoped markets, and competitor=Team for team-scoped economy markets. You can safely ignore them.
Market Status Codes
| Code | Status | Description |
|---|
1 | Active | Bets can be accepted, odds are flowing |
0 | Deactivated | Odds not shown, bets not accepted |
-1 | Suspended | Stop accepting bets immediately |
-3 | Settled | Result known — only in <bet_settlement> messages |
-4 | Cancelled | Market voided — return stakes — only in <bet_cancel> messages |
When a market status changes to -1 (suspended), you must immediately stop accepting bets. Do not wait for settlement.
Bet Settlement
Published when markets are resulted.
<bet_settlement product="2" timestamp="1711234590123"
event_id="od:match:2588141"
tt_match_id="98deb7ef-def9-47d4-854c-a13618952f99">
<outcomes>
<market id="1013"
tt_market_id="b7e2c3d5-6789-4e0f-c123-bcdef2345678"
type="grenade_kill"
specifiers="map=1|round=5"
status="-3"
textEN="Grenade kill in Round 5"
textRU="Убийство гранатой в раунде 5">
<outcome id="1" result="1"/>
</market>
</outcomes>
</bet_settlement>
result value | Meaning |
|---|
1 | Winning outcome — pay out |
0 | Losing outcome |
If a market is voided via settlement (e.g. match cancellation), the outcome may include void_factor="1.0" — return the full stake to the bettor:
<outcome id="1" result="0" void_factor="1.0"/>
When a market is settled, it is automatically removed from all subsequent <odds_change> messages.
Bet Cancel
Published when a market is voided — return all stakes.
<bet_cancel product="2" timestamp="1711234600000"
event_id="od:match:2588141"
tt_match_id="98deb7ef-def9-47d4-854c-a13618952f99">
<market id="1013"
tt_market_id="b7e2c3d5-6789-4e0f-c123-bcdef2345678"
type="grenade_kill"
specifiers="map=1|round=5"
void_reason_id="4"
void_reason_params="custom=not_played"
textEN="Grenade kill in Round 5"
textRU="Убийство гранатой в раунде 5" />
</bet_cancel>
Rollback Bet Settlement
Reverses a previous settlement due to an error. Rare — you will also be notified via your support channel.
<rollback_bet_settlement product="2" timestamp="1711234610000"
event_id="od:match:2588141"
tt_match_id="98deb7ef-def9-47d4-854c-a13618952f99">
<market id="1013"
tt_market_id="b7e2c3d5-6789-4e0f-c123-bcdef2345678"
type="grenade_kill"
specifiers="map=1|round=5" />
</rollback_bet_settlement>
Fixture Change
Notifies about match lifecycle events. Re-fetch the fixture from the REST API when you receive this message.
<fixture_change product="2" timestamp="1711234500000"
event_id="od:match:2588141"
tt_match_id="98deb7ef-def9-47d4-854c-a13618952f99"
change_type="1"/>
change_type | Meaning |
|---|
1 | New fixture with markets |
2 | Start time changed |
3 | Match cancelled |
5 | Coverage changed / match finished |
Alive
Heartbeat sent every 10 seconds per producer. Use it to validate feed connectivity.
<alive product="2" timestamp="1711234570000" subscribed="1"/>
subscribed | Meaning |
|---|
1 | Normal — feed is healthy |
0 | Error or producer restart — suspend all markets and initiate recovery |
If you do not receive an <alive> message within 20 seconds, consider the connection stale. Suspend all markets and reconnect.
Market Ordering
Each <odds_change> message delivers up to 3 markets per round, already sorted in the recommended display order.
The feed applies two selection constraints:
- Diversity — each market comes from a different category (e.g. opening kills, bomb events, round dynamics) to give bettors variety.
- Timing order — markets are ordered earliest-resolving first. The first market in the message resolves soonest in the round, the last resolves latest. This minimises risk from feed-delivery delays.
| Position in message | Resolves | Typical categories |
|---|
| First | Early in round | Opening kills, weapon kills |
| Second | Mid-round | Multi-kills, round dynamics, highlight kills |
| Third | Late in round | Bomb events, economy, flawless |
Recommended display pattern: show the first market in the message prominently and reveal subsequent markets as the bettor interacts. The feed’s ordering already reflects what to surface first — no custom sorting logic needed.
Localisation
Every <market> element carries both English and Russian descriptions:
| Attribute | Language | Example |
|---|
textEN | English | ”Grenade kill in Round 4” |
textRU | Russian | ”Убийство гранатой в раунде 4” |
Outcome elements carry active, odds, and probabilities attributes — localised text is on the parent <market> element only.
Simplest integration path: take textEN (or textRU) directly from the message and display it to the end-user. No template engine, no catalogue look-up, no string interpolation — the text is ready to show as-is.Template path: for operators who prefer to build market names locally, fetch the catalogue once at integration time from GET /cs2/v1/markets/descriptions (or descriptions.xml for Oddin-style XML) — every entry carries template and template_ru strings such as "{player} gets opening kill — Round {round}". Substitute the placeholders with the matching keys from the specifiers attribute on each live message:| Placeholder | Specifier | Notes |
|---|
{round} | round | always present |
{player} | player | player-scoped markets (player_opening, awp_player_kill, star_multi_kill) |
{competitor} | competitor | team-scoped economy markets (eco_upset, eco_frags, force_buy_win) |
{map_name} | map_name | reserved for future map-scoped templates; the specifier itself is already populated when the map name is known |
Reconnection and recovery
RabbitMQ persistent queues retain messages while your consumer is disconnected.
Messages delivered to a durable queue are not lost if your service restarts — they will be re-delivered when you reconnect.
Use aio_pika.connect_robust (or equivalent in your AMQP client) for automatic reconnection with exponential backoff.
After reconnecting, call POST /cs2/v1/recovery/initiate_request with the timestamp of the last message you processed. The server republishes the current state of every active market plus settlements and voids that happened since after, ending with a <snapshot_complete request_id="…"> message so you know you’re caught up. Recovery messages reuse the same exchange as the live feed, carry a recovery=1 header, and use routing-key priority lo (vs live hi) — you can bind a separate queue if you want to route them differently.
Code Example
The following example uses aio-pika (Python asyncio AMQP client):
import asyncio
import aio_pika
from xml.etree import ElementTree
AMQP_URL = "amqp://op-acme-sports:<password>@mq.ticktock.bet:5672/acme-sports"
EXCHANGE = "ttfeed"
async def main():
connection = await aio_pika.connect_robust(AMQP_URL)
channel = await connection.channel()
exchange = await channel.declare_exchange(
EXCHANGE, aio_pika.ExchangeType.TOPIC, durable=True, passive=True,
)
queue = await channel.declare_queue("ticktock-all", durable=True)
await queue.bind(exchange, routing_key="hi.-.live.#")
await queue.bind(exchange, routing_key="-.-.-.alive.-.-.-.-")
async with queue.iterator() as q:
async for message in q:
async with message.process():
xml_body = message.body.decode()
routing_key = message.routing_key
# Extract message type from 4th section of routing key
msg_type = routing_key.split(".")[3] if routing_key else "unknown"
if msg_type == "odds_change":
root = ElementTree.fromstring(xml_body)
event_id = root.get("event_id")
for market in root.findall(".//market"):
status = market.get("status")
text = market.get("textEN")
if status == "-1":
suspend_market(market.get("id"))
elif status == "1":
for outcome in market.findall("outcome"):
update_odds(market.get("id"), text, outcome.get("odds"))
elif msg_type == "bet_settlement":
root = ElementTree.fromstring(xml_body)
for market in root.findall(".//market"):
for outcome in market.findall("outcome"):
grade_bet(market.get("id"), outcome.get("result"))
elif msg_type == "alive":
pass # heartbeat — feed is healthy
asyncio.run(main())
Support
For credentials, connectivity issues, or integration questions: b2b@ticktock.bet