Quick Start: Aggregator Integration
This guide takes you from zero to a live Prediction Markets game in your catalog. You call the Launcher to start a session for a player, and Round/Details to read what a bet is on. You then expose four wallet endpoints, which Prediction Markets calls to debit each bet and to credit each win.
Follow the steps in order. Each step builds on the step before it.
Are you a single operator, and not an aggregator? Use the Standalone Quick Start. The two paths use different endpoints and a different signature header.
Prerequisites
Before you start, make sure that you have:
- A server that Prediction Markets can call over HTTPS
- A player wallet that you can read, debit, and credit by account identifier and currency
- These values from Prediction Markets:
| Value | Description |
|---|---|
LAUNCHER_BASE_URL | The host for every a8r_provider path: the two Launcher paths and Round/Details. One host serves all three. |
SHARED_SECRET | The key for the X-REQUEST-SIGN signature, in both directions |
| Source IP addresses | Optional. Ask for these if you filter incoming requests by address. |
The examples on this page use $LAUNCHER_BASE_URL and $SHARED_SECRET for these two values. Every host in an example is a placeholder. Use the values that Prediction Markets gives you.
The game identifier is prediction-markets. Send this value in the game field of each Launcher request. Prediction Markets sends the same value in the game_id field of each wallet request.
Give Prediction Markets your wallet base URL. Prediction Markets adds the four fixed paths to it.
How a round works
Read this before you write any code. A round in Prediction Markets is different from a round in a casino game, and the difference decides how you build your wallet.
In a casino game, a round is one play. The player makes a bet, the game shows a result, and the round closes. This takes a few seconds.
In Prediction Markets, a round is one bet on one market outcome:
- The round opens when the player places the bet.
- The round closes when the market resolves.
The time between the two events is the life of the market. A market on a football match resolves in hours. A market on an election resolves in months. The player does not wait in the game for the result.
Four rules follow from this. Build them in from the start:
| Rule | Reason |
|---|---|
| Hold many open rounds for one player at the same time | A player can hold 20 bets on 20 different markets |
| Never close a round on a timer | Only Prediction Markets knows when the market resolves |
| Never reject a request because the round is old | A settlement can arrive months after the bet |
Match a settlement to its bet by round_id only | The two requests can be months apart |
session_id in a Round request can be deadPrediction Markets stores the session_id with the bet, and sends the same value again at settlement. In most settlements the player closed the game long before, so that session no longer exists.
Use session_id for reports and for support. Never use it to authorize a Round request, and never reject a request because the session expired. Your wallet must accept a settlement for a player who is not in the game.
For the full protocol, see Aggregator Wallet Integration.
Step 1: Call the Launcher
Call Launcher/Real to start a real-money session. Sign the body, then load the launch_url from the response in an iframe on the casino page.
curl -X POST "$LAUNCHER_BASE_URL/v2/a8r_provider.Launcher/Real" \
-H "Content-Type: application/json" \
-H "X-REQUEST-SIGN: 37f9186da8bef5457f94d56d1c76dc37..." \
-d '{
"casino_id": "casino-7",
"game": "prediction-markets",
"locale": "en",
"ip": "203.0.113.42",
"client_type": "desktop",
"session_id": "3f2b7c1e-8a4d-4e6b-9c11-2d5a7e9f0b31",
"urls": {},
"account": {
"id": "player-42",
"currency": "USD"
}
}'
{
"launch_url": "https://game.example.com/?session_request_id=...&locale=en"
}
The host in launch_url is an example. Prediction Markets gives you the real host, and it can be different for the test environment and for production. Read the host from the response, and do not write it into your code.
Rules for the Launcher:
- Always send
session_id. The base Game Aggregator contract makes this field optional. Prediction Markets rejects a missing value or a blank value with status code 400. - Use a UUID for
session_id. Prediction Markets sends the same value back in each wallet request for the session. - Load
launch_urlin an iframe. The game runs in an iframe only. It does not work as a top-level page. - The URL holds a single-use token with a life of about one minute. Get a new
launch_urlfor each new game session. Do not open the same URL a second time, and do not put it in a log or in an analytics call. - Loading the iframe is all the casino page does. Our frontend code in the iframe then exchanges the token for a session, and it calls our API gateway to do it. You write no code for that exchange.
- The
urlsobject is required, but both fields inside it are optional. Prediction Markets does not usereturn_urlordeposit_urltoday, so"urls": {}is enough. localeacceptsen,es,nl,fr,de, andru. A code with a region also works, sode-atandDE_ATboth give German. Any other language gives English.- The
accountobject needsidandcurrencyonly. The other fields in it are optional, and Prediction Markets does not use them today. The example above sends the minimum. - The API gateway accepts 5 requests each second for each IP address. Above this rate, it returns status code 429.
The Launcher error body has one field, and it is not the body that the wallet endpoints use:
{ "error": "invalid_request" }
Call Launcher/Demo for a demo session. A demo session has no account object and no session_id. Prediction Markets creates no session and calls no wallet endpoint.
Sign the request body
signature = HMAC-SHA256(raw_request_body, SHARED_SECRET)
header = hex_encode(signature)
const crypto = require('crypto');
function signRequest(rawBody) {
return crypto
.createHmac('sha256', process.env.SHARED_SECRET)
.update(rawBody)
.digest('hex');
}
Test your code with these values:
- Body:
{"amount":"10.50"} - Secret:
test-secret - Signature:
37f9186da8bef5457f94d56d1c76dc37f8c8854e35751cf7eb795da23d593329
Step 2: Read the Round Details
The wallet requests carry no bet details. Round/BetWin holds an identifier, a type and an amount, and nothing more. To show the player which event, which market and which selection the bet is on, read the details from Prediction Markets.
You call this endpoint on the Launcher host, with the same signature.
curl -X POST "$LAUNCHER_BASE_URL/v2/a8r_provider.Round/Details" \
-H "Content-Type: application/json" \
-H "X-REQUEST-SIGN: 37f9186da8bef5457f94d56d1c76dc37..." \
-H "Accept-Language: pt" \
-d '{
"casino_id": "casino-7",
"round_id": "8e1f5a02-3f4d-4d9e-9b2c-1a2b3c4d5e6f",
"locale": "pt"
}'
{
"round_id": "8e1f5a02-3f4d-4d9e-9b2c-1a2b3c4d5e6f",
"round_details": {
"account_id": "player-42",
"bet_type": "single",
"status": "won",
"event": { "id": "0d7c1e22-...", "title": { "pt": "Eleições EUA 2024" } },
"market": { "id": "5fe1d4a7-...", "question": { "pt": "A equipa A vai vencer?" } },
"selection": { "id": "a93be5c2-...", "name": { "pt": "Sim" } },
"stake": "5.00",
"currency": "EUR",
"odds": "1.85",
"placed_at": "2026-04-25T10:10:00Z",
"payout": "9.25",
"settled_at": "2026-04-25T10:15:30Z",
"rejection_code": null
}
}
Call it after each wallet request from Prediction Markets. Each wallet request marks a state change:
| Prediction Markets calls | The round state becomes |
|---|---|
Round/BetWin, type=bet | pending |
Round/BetWin, type=win | won |
Round/Finish | lost |
Round/Rollback | rejected or voided |
Prediction Markets calls Round/BetWin to perform the debit. At the moment you receive that request, the round is still pending. Prediction Markets changes the state only after you answer 200.
From inside your handler you read pending and store incorrect data. Answer the wallet request first, then call Round/Details.
Rules for Round/Details:
round_idis the same value that Prediction Markets sends inRound/BetWinandRound/Rollback.- Do not calculate the payout from the stake and the odds. Prediction Markets rounds
oddsto two decimal places for display, sostakemultiplied byoddscan differ frompayout. Show thepayoutvalue. - A
rejectedround holds the event, the market, the selection and the money values. One request is sufficient to show the bet that the player tried to make. rejection_codeis an open set. Treat a value that you do not know as "other". Do not write a closed list of values in your code.- Prediction Markets reads the text when you send the request. If an administrator edits a title or a translation after the bet, the player sees the new text.
- The path has no rate limit. The Launcher limit of 5 requests each second for each IP address does not apply here.
localeis optional, and theAccept-Languageheader is the second source. The default isen. The supported locales areen,es,pt,frandde.
An error uses the same body as the wallet endpoints. meta.api_code gives the reason: 400 for a request that is not valid, 404 for a round that does not exist, and 503 when Prediction Markets cannot read the round.
For the field reference and the full error table, see Round details.
Step 3: Expose the Four Wallet Paths
Prediction Markets calls four endpoints on your server. Expose all four at these exact paths, under your wallet base URL:
| Path | When Prediction Markets calls it | Purpose |
|---|---|---|
POST /v2/provider_a8r.Player/Balance | Session start, each profile read, and before each bet | Read the player's balance |
POST /v2/provider_a8r.Round/BetWin | Bet placement, and settlement of a win | Debit a stake, or credit a win |
POST /v2/provider_a8r.Round/Finish | Settlement of a loss | Close a round that has no win |
POST /v2/provider_a8r.Round/Rollback | After a failed debit or credit | Reverse a transaction |
The provider_a8r prefix is part of the path, and no setting changes it. If you rename it, every wallet call returns 404.
For the reason that these names contain a8r, see Names that contain a8r.
Verify the signature
Every request from Prediction Markets contains an X-REQUEST-SIGN header. Verify it before you process the request. Reject a bad signature with status code 403.
const crypto = require('crypto');
function verifyPredictionMarketsRequest(req, res, next) {
const signature = req.headers['x-request-sign'];
if (!signature) {
return res.status(403).json({ error: 'Missing signature' });
}
const expected = crypto
.createHmac('sha256', process.env.SHARED_SECRET)
.update(req.rawBody) // the raw bytes, not the parsed JSON
.digest('hex');
const expectedBuf = Buffer.from(expected, 'hex');
const actualBuf = Buffer.from(signature, 'hex');
if (
expectedBuf.length !== actualBuf.length ||
!crypto.timingSafeEqual(expectedBuf, actualBuf)
) {
return res.status(403).json({ error: 'Invalid signature' });
}
next();
}
Use the exact bytes of the body. If you build the JSON again first, the signature is different and the request fails with status code 403.
Many web frameworks parse the body before your code runs. Configure your framework to keep the original bytes. In Express, use the verify option of express.json().
This scheme has no timestamp and no nonce value. Read No replay protection for the controls that limit the effect.
Step 4: Implement Player/Balance
Return the current balance for the account_id and the currency in the request.
# Prediction Markets sends:
POST /v2/provider_a8r.Player/Balance
Content-Type: application/json
X-REQUEST-SIGN: 37f9186da8bef5457f94d56d1c76dc37...
{
"account_id": "player-42",
"currency": "USD",
"game_id": "prediction-markets",
"session_id": "3f2b7c1e-8a4d-4e6b-9c11-2d5a7e9f0b31"
}
# Your server responds:
HTTP 200
{ "balance": "489.50" }
Return the balance as a decimal string. Use a maximum of 12 digits after the decimal point. Do not return the balance as a number. A crypto balance can have more digits than a 64-bit float can hold.
Prediction Markets calls this endpoint at the start of the session, each time the game reads the player profile, and before each bet. An error at the start of the session stops the player from opening the game. An error before a bet does not stop the bet.
Step 5: Implement Round/BetWin
This endpoint debits a stake and credits a win. The type field of each transaction selects the action. Apply the four rules from How a round works here.
# Prediction Markets sends a bet:
POST /v2/provider_a8r.Round/BetWin
Content-Type: application/json
X-REQUEST-SIGN: <signature>
{
"account_id": "player-42",
"currency": "USD",
"game_id": "prediction-markets",
"round_id": "bet_01HZABC...",
"finished": false,
"session_id": "3f2b7c1e-8a4d-4e6b-9c11-2d5a7e9f0b31",
"transactions": [
{ "id_provider": "txn_01HZABC...", "type": "bet", "amount": "10.50" }
]
}
# Your server responds:
HTTP 200
{
"round_id": "your-internal-round-id",
"balance": "479.00",
"transactions": [
{ "id_provider": "txn_01HZABC...", "id": "your-internal-txn-id" }
]
}
Rules for this endpoint:
- Return one item for each transaction in the request, in the same order. Copy
id_providerto each item. - Return
round_idandbalance. If either field is absent, Prediction Markets reads the response as a failure and sends aRound/Rollbackrequest. - Do not reject a transaction only because the round is already closed.
- A transaction amount is never 0.
A win uses the same endpoint with type set to win and finished set to true. A void bet or a refund is also a win transaction, for the amount of the stake.
Prediction Markets sends the bet transaction for a round before the win transaction, so a win with no earlier bet cannot happen now.
Accept that order anyway. If Prediction Markets adds free bets or bonuses later, a win can arrive for a round that had no debit.
Idempotency
Prediction Markets can send the same id_provider a second time after a timeout or a network error. Your endpoints must be idempotent. If you receive an id_provider that you processed before, do not process it a second time. Return the response that you returned the first time.
Rejections and error codes
To reject a bet for a business reason, set meta.api_code in the error body:
HTTP 400
{
"code": "invalid_argument",
"msg": "Not enough funds.",
"meta": {
"api_code": "100",
"api_message": "The player does not have enough money for this action.",
"balance": "0.01"
}
}
Send api_code as a decimal number in a string. These are the codes that Prediction Markets understands:
api_code | Meaning | What Prediction Markets does |
|---|---|---|
100 | The player does not have enough money | Rejects the bet. Tells the player that the balance is too low. |
105 | The bet is above a limit in your system | Rejects the bet. Tells the player about the limit. |
503 | Your system is not available | Sends the request again later |
Every other value falls into one of two groups, by number:
api_code | Group | What Prediction Markets does |
|---|---|---|
100 to 199 | A final rejection | Rejects the bet and stops |
400 to 499 | A final rejection | Rejects the bet and stops |
| Any other number | A temporary error | Sends the same request again |
Use 100 and 105 where they apply. The player then sees the correct reason instead of a general message.
Prediction Markets reads api_code with a numeric parse. A value such as "INSUFFICIENT_FUNDS" fails that parse, so Prediction Markets puts it in the temporary group.
Prediction Markets then sends your final rejection again and again. Your wallet rejects it again and again. The player waits for an answer that never comes. Always send a number.
Prediction Markets also reads meta.api_code in a response with status code 200.
Step 6: Implement Round/Finish and Round/Rollback
Round/Finish closes a round that has no win. Prediction Markets sends it for a lost bet, because a lost bet has no money to move.
# Prediction Markets sends:
POST /v2/provider_a8r.Round/Finish
{
"account_id": "player-42",
"currency": "USD",
"round_id": "bet_01HZABC...",
"session_id": "3f2b7c1e-8a4d-4e6b-9c11-2d5a7e9f0b31"
}
# Your server responds:
HTTP 200
{ "balance": "479.00" }
Round/Rollback reverses a transaction. Prediction Markets sends it after a Round/BetWin request failed or stopped after a timeout.
# Prediction Markets sends:
POST /v2/provider_a8r.Round/Rollback
{
"account_id": "player-42",
"currency": "USD",
"game_id": "prediction-markets",
"round_id_provider": "bet_01HZABC...",
"finished": false,
"session_id": "3f2b7c1e-8a4d-4e6b-9c11-2d5a7e9f0b31",
"transactions": [
{
"id_provider": "rbk_01HZABC...",
"type": "rollback",
"original_id_provider": "txn_01HZABC..."
}
]
}
# Your server responds:
HTTP 200
{
"balance": "489.50",
"round_id": "your-internal-round-id",
"transactions": [
{ "id_provider": "rbk_01HZABC...", "id": "your-internal-txn-id" }
]
}
Rules for Round/Rollback:
- Process a rollback even if it arrives before the matching
Round/BetWintransaction. - If the original transaction does not exist, return a valid response. Do not return an error. Set
idto an empty string. - After you reverse a transaction, do not process a late copy of the original
Round/BetWinrequest.
Go-Live Checklist
Run through this list before you open the game for real players:
- All four wallet paths are live at the exact
provider_a8rpaths -
Player/Balancereturns the balance as a decimal string -
Round/BetWinreturnsround_id,balance, and one item for each transaction -
Round/Finishcloses a round and returns the balance -
Round/Rollbackreturns a valid response for a transaction that does not exist - All four endpoints are idempotent by
id_provider - Your wallet holds many open rounds for one player at the same time
- Your wallet accepts a settlement for a player who is not in the game
- Your wallet does not reject a
Roundrequest because the round is old, or becausesession_idrefers to a session that expired - Your error responses send
api_codeas a number, and use100and105where they apply - Signature verification is active on all four endpoints, and rejects a bad signature with 403
- Signature verification uses the raw request bytes
- Your Launcher requests are signed with the same secret
- You call
Round/Detailsafter you answer each wallet request, and never from inside the handler - You show the
payoutfromRound/Details, and never calculate it from the stake and the odds - Every Launcher request sends a UUID
session_id - Your casino page loads
launch_urlin an iframe - You tested a full bet, a win, a loss, and a rollback in the test environment
See Also
- Aggregator Wallet Integration — the full protocol, rounds, and the retry rules
- Aggregator API Reference — request and response schemas
- Delivery Options — a comparison of the three integration paths