Skip to main content

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:
ValueDescription
LAUNCHER_BASE_URLThe host for every a8r_provider path: the two Launcher paths and Round/Details. One host serves all three.
SHARED_SECRETThe key for the X-REQUEST-SIGN signature, in both directions
Source IP addressesOptional. 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:

RuleReason
Hold many open rounds for one player at the same timeA player can hold 20 bets on 20 different markets
Never close a round on a timerOnly Prediction Markets knows when the market resolves
Never reject a request because the round is oldA settlement can arrive months after the bet
Match a settlement to its bet by round_id onlyThe two requests can be months apart
session_id in a Round request can be dead

Prediction 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_url in 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_url for 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 urls object is required, but both fields inside it are optional. Prediction Markets does not use return_url or deposit_url today, so "urls": {} is enough.
  • locale accepts en, es, nl, fr, de, and ru. A code with a region also works, so de-at and DE_AT both give German. Any other language gives English.
  • The account object needs id and currency only. 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 callsThe round state becomes
Round/BetWin, type=betpending
Round/BetWin, type=winwon
Round/Finishlost
Round/Rollbackrejected or voided
Call it after you answer the wallet request

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_id is the same value that Prediction Markets sends in Round/BetWin and Round/Rollback.
  • Do not calculate the payout from the stake and the odds. Prediction Markets rounds odds to two decimal places for display, so stake multiplied by odds can differ from payout. Show the payout value.
  • A rejected round 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_code is 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.
  • locale is optional, and the Accept-Language header is the second source. The default is en. The supported locales are en, es, pt, fr and de.

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:

PathWhen Prediction Markets calls itPurpose
POST /v2/provider_a8r.Player/BalanceSession start, each profile read, and before each betRead the player's balance
POST /v2/provider_a8r.Round/BetWinBet placement, and settlement of a winDebit a stake, or credit a win
POST /v2/provider_a8r.Round/FinishSettlement of a lossClose a round that has no win
POST /v2/provider_a8r.Round/RollbackAfter a failed debit or creditReverse a transaction
The paths are literal

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();
}
Sign and verify the raw body

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_provider to each item.
  • Return round_id and balance. If either field is absent, Prediction Markets reads the response as a failure and sends a Round/Rollback request.
  • 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.

A win always follows its bet today

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_codeMeaningWhat Prediction Markets does
100The player does not have enough moneyRejects the bet. Tells the player that the balance is too low.
105The bet is above a limit in your systemRejects the bet. Tells the player about the limit.
503Your system is not availableSends the request again later

Every other value falls into one of two groups, by number:

api_codeGroupWhat Prediction Markets does
100 to 199A final rejectionRejects the bet and stops
400 to 499A final rejectionRejects the bet and stops
Any other numberA temporary errorSends the same request again

Use 100 and 105 where they apply. The player then sees the correct reason instead of a general message.

A code that is not a number is read as a temporary error

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/BetWin transaction.
  • If the original transaction does not exist, return a valid response. Do not return an error. Set id to an empty string.
  • After you reverse a transaction, do not process a late copy of the original Round/BetWin request.

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_a8r paths
  • Player/Balance returns the balance as a decimal string
  • Round/BetWin returns round_id, balance, and one item for each transaction
  • Round/Finish closes a round and returns the balance
  • Round/Rollback returns 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 Round request because the round is old, or because session_id refers to a session that expired
  • Your error responses send api_code as a number, and use 100 and 105 where 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/Details after you answer each wallet request, and never from inside the handler
  • You show the payout from Round/Details, and never calculate it from the stake and the odds
  • Every Launcher request sends a UUID session_id
  • Your casino page loads launch_url in an iframe
  • You tested a full bet, a win, a loss, and a rollback in the test environment

See Also​