Skip to main content

Aggregator Wallet Integration

Prediction Markets is one game in your catalog. Your platform starts the game for a player. Prediction Markets then calls your wallet to debit each bet and to credit each win.

Are you a single operator, and not an aggregator? Use the Standalone path. The two paths use different endpoints and a different signature header.

Two directions​

The connection has two directions. Each direction has its own path prefix.

DirectionPrefixEndpointsWho implements the endpoints
You call Prediction Marketsa8r_providerLauncher/Real, Launcher/Demo, Round/DetailsPrediction Markets
Prediction Markets calls youprovider_a8rPlayer/Balance, Round/BetWin, Round/Finish, Round/RollbackYou

The two prefixes are not interchangeable. Each prefix applies to one direction only.

Read the prefix to find the direction. Round/Details and Round/BetWin share a name and go in opposite directions. You call Round/Details. Prediction Markets calls Round/BetWin.

Names that contain a8r​

The paths and the signature header contain the text a8r. Prediction Markets built this connection first for one aggregator, A8R, and the names stayed. They are the same for every aggregator partner.

These names are not placeholders. Write each name as shown. Do not replace a name with the name of your company or your system.

NameWhere it appears
/v2/a8r_provider.Launcher/Real
/v2/a8r_provider.Launcher/Demo
/v2/a8r_provider.Round/Details
The URLs you call
/v2/provider_a8r.Player/Balance
/v2/provider_a8r.Round/BetWin
/v2/provider_a8r.Round/Finish
/v2/provider_a8r.Round/Rollback
The four paths you expose
X-REQUEST-SIGNThe signature header, in both directions
The four wallet paths are literal

You give Prediction Markets one base URL. Prediction Markets adds each fixed path to it:

https://wallet.your-company.example.com  +  /v2/provider_a8r.Round/BetWin
= https://wallet.your-company.example.com/v2/provider_a8r.Round/BetWin

There is no setting that changes the path. If you rename it, every wallet call returns 404.

Flow​

Four parts take part in the flow:

PartWho owns itWhat it does
Casino siteYour clientThe page the player uses. It loads the game in an iframe, and it does nothing more.
GamePrediction MarketsOur frontend code. It runs in the iframe on the casino page.
Your systemYouCalls the Launcher and Round/Details, and answers the four wallet paths. One service or two, as you prefer.
Prediction MarketsPrediction MarketsOur backend and our API gateway.

Each Round/Details arrow goes from your system to Prediction Markets. Each other Round arrow goes to your system.

Step 2 is the part that is easy to get wrong. The casino site only loads the iframe. Our frontend code in the iframe then takes the short-lived session_request_id from the launch URL and sends it to our API gateway, which returns a session token.

Your system writes no code for step 2. You see one Player/Balance request from it, and nothing else.

Launch​

Real money​

Call POST /v2/a8r_provider.Launcher/Real to start a real-money session. Prediction Markets creates the bettor from the account object, keeps a single-use launch token, and returns a launch_url. Load that URL in an iframe on the casino page.

You must always send session_id. The base Game Aggregator contract makes this field optional, but Prediction Markets cannot identify the session without it. Prediction Markets rejects a missing value or a blank value with status code 400.

Prediction Markets sends the same session_id back in each Player request and each Round request for the session.

Demo​

Call POST /v2/a8r_provider.Launcher/Demo to start a demo session. A demo session has no account object and no session_id. Prediction Markets creates no session and no bettor, and calls no wallet endpoint. The player cannot place a real bet.

The launch URL​

Load the launch_url in an iframe. The game runs in an iframe only. It does not work as a top-level page.

The URL contains a single-use launch token with a life of about one minute. The game exchanges the token one time, when it starts. Get a new launch_url for each new game session. Do not open the same launch_url a second time, and do not put it in a log or in an analytics call.

The urls object​

The contract requires the urls object, but both fields inside it are optional. Prediction Markets does not use either field today, so you can send an empty object:

{ "urls": {} }
FieldStatus
return_urlPrediction Markets accepts it and does not use it. The game shows no home button.
deposit_urlPrediction Markets accepts it and does not use it. The game shows no deposit button.

Send the fields if it is easier for you. A later version of the game can use them.

Errors and limits​

The Launcher endpoints use an error body that is different from the error body of the Player endpoints and the Round endpoints:

{ "error": "forbidden" }

The body has no code field, no msg field, and no meta field. The value of error is a short code, for example invalid_request, forbidden, or internal_error.

The API gateway limits each IP address to 5 requests each second. Above this rate, the gateway returns status code 429. The gateway makes this response, so the body is different again.

The Launcher endpoints go through the API gateway, but the gateway does not authenticate them. The gateway applies the rate limit only. Prediction Markets verifies the X-REQUEST-SIGN signature in the application.

Rounds​

Read this section before you write the Round endpoints. A round in Prediction Markets is different from a round in a casino game. The difference changes how your wallet must behave.

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.

round_id holds the identifier of the bet in the Prediction Markets system. It is the only link between the bet and the settlement, so keep it.

What a long round means for your wallet​

RuleReason
Accept many open rounds for one player at the same timeA player can hold 20 bets on 20 different markets
Do not close a round on a timerOnly Prediction Markets knows when the market resolves
Do not reject a Round/BetWin request because the round is oldA settlement can arrive months after the bet
Correlate the settlement with the bet by round_idThe two requests can be months apart
Do not check session_id against a live sessionSee the warning below
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. Do not use it to authorize a Round request, and do not reject a request because the session expired. Your wallet must accept a settlement for a player who is not in the game.

The four Round calls​

CallEffect on the roundfinished
Round/BetWin, type=betOpens the roundfalse
Round/BetWin, type=winCloses the roundtrue
Round/FinishCloses a round that has no win—
Round/RollbackReverses a transactionalways false

Prediction Markets sets finished to true on a win so that it does not have to send a separate Round/Finish request.

A lost bet has no money to move. Prediction Markets sends Round/Finish for that round instead of a credit for 0.

A void bet or a refund is a win transaction for the amount of the stake. There is no separate endpoint for a refund.

Each request has one transaction. Do not reject a transaction only because the round is already closed.

Round details​

The wallet requests carry no bet details. Round/BetWin holds an identifier, a type and an amount for each transaction, and nothing more. To show the player which event, which market and which selection the bet is on, read the details from Prediction Markets:

POST /v2/a8r_provider.Round/Details

Prediction Markets implements this path, and it is the only Round path that you call. Send round_id. Prediction Markets answers with the event, the market, the selection, the money values and the state of that round.

When to call it​

Call Round/Details 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.

This keeps the number of requests low. You send one request for each state change, whatever the number of times a player opens the bet history.

The request​

{
"casino_id": "sample_casino",
"round_id": "8e1f5a02-3f4d-4d9e-9b2c-1a2b3c4d5e6f",
"locale": "pt"
}

round_id is the same value that Prediction Markets sends in Round/BetWin and Round/Rollback.

Prediction Markets accepts casino_id and does not use it.

locale is optional. Prediction Markets reads it first, then the Accept-Language header, then uses en. The supported locales are en, es, pt, fr and de.

The response​

{
"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
}
}

Every field is present. The fields payout, settled_at and rejection_code can hold null.

FieldNotes
account_idThe player account that made the bet. The value is the one you gave at launch.
bet_typeThe kind of bet. The value is single, because one round holds one selection.
statusOne of pending, rejected, won, lost, voided
event, market, selectionEach holds an id and a text field. The text is a map with one entry, and the key is the resolved locale.
stake, currencyThe amount the player bet, and its currency
oddsThe odds at the time of the bet, with two decimal places
placed_atThe time of the bet
payoutThe amount Prediction Markets paid. null for pending and rejected, 0.00 for lost, equal to the stake for voided.
settled_atThe time of the settlement. null for pending and rejected.
rejection_codeThe reason for a rejected round. null for every other state.
Do not calculate the payout from the stake and the odds

odds is a value to show to the player, rounded to two decimal places. The odds stored with the bet can hold more digits, for example 1.8537. The response then holds odds: "1.85", stake: "5.00" and payout: "9.27".

payout holds the correct amount. Show that value. If you calculate the payout from the stake and the odds, you show the player an amount that Prediction Markets did not pay.

A rejected round still holds the event, the market, the selection, the stake, the currency, the odds and the placement time. You can show the bet that the player tried to make without a second request.

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. These are the current values:

ValueMeaning
insufficient_fundsThe player's wallet held too little money
bet_limit_reachedThe player passed a limit
bettor_blockedThe casino suspended the player
validation_failedA check failed before the wallet call
wallet_provider_unavailableThe wallet did not answer
wallet_rolled_backThe bet workflow reversed the debit
bet_rejected_otherAny other final reason

Errors​

The endpoint answers for a round in every state. There is no "not ready" condition.

ConditionHTTPcodemeta.api_codemeta.api_message
The body is not valid400invalid_argument400Invalid request.
The signature is not valid403——As the Launcher endpoints answer
No round has that identifier400invalid_argument404Not found.
Prediction Markets cannot read the round500internal503Service is unavailable.
{
"code": "invalid_argument",
"msg": "Not found.",
"meta": {
"api_code": "404",
"api_message": "Not found."
}
}

The API gateway applies no rate limit to this path. The Launcher limit of 5 requests each second for each IP address does not apply here.

The text can change after the bet

Prediction Markets reads the event title, the market question and the selection name when you send the request. There is no copy stored with the bet.

An administrator can edit a title or a translation after the player places the bet. The round details then hold the new text, and the player sees text that is different from the text at the time of the bet.

Wallet protocol​

What Prediction Markets calls, and when​

Prediction Markets actionEndpointKey fields
Start of the sessionPlayer/Balance—
Read the player profile in the gamePlayer/Balance—
Before each betPlayer/Balance—
Debit a stakeRound/BetWintype=bet, finished=false
Credit a winRound/BetWintype=win, finished=true
Close a lost betRound/Finish—
Reverse a failed debit or creditRound/Rollbacktype=rollback, finished=false

Prediction Markets calls Player/Balance in three places, and the three calls have different rules:

  • At the start of the session, the call is also a test of the session. An error fails the session, and the player cannot open the game.
  • When the game reads the player profile, the call gives the balance for the display. An error hides the balance. The game shows the player an error, or asks the player to sign in again.
  • Before each bet, the call is a check for sufficient money. An error does not stop the bet. Prediction Markets continues to Round/BetWin, which is the authority for the debit.

There is no session endpoint and no authentication endpoint in this protocol. The Launcher call is the authentication. Prediction Markets gets the player identity and the currency from the account object in that call.

Amounts and balances​

Send and receive each amount as a decimal string. Use a maximum of 18 digits before the decimal point and 12 digits after it.

A transaction amount is never 0. Prediction Markets rounds a payout to 12 digits after the decimal point, and removes the zeros at the end.

Return the balance as a decimal string, in the same format. Do not return the balance as a number. A crypto balance can have more digits than a 64-bit float can hold.

The currency code is 3 to 7 uppercase letters or digits. Fiat currencies use ISO 4217 codes. Crypto assets use extended codes. Do not reject a code because it has more than 3 characters.

Idempotency​

Each transaction has an id_provider value from Prediction Markets. Prediction Markets can send the same id_provider a second time after a timeout or a network error.

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.

Your response must contain balance, round_id, and one item for each transaction in the request. If one of these is absent, Prediction Markets reads the response as a failure. Prediction Markets then sends a Round/Rollback request.

Rejections and retries​

To reject a request for a business reason, set meta.api_code in the error body:

{
"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"
}
}

Prediction Markets reads meta.api_code in every response, and also in a response with status code 200.

Error codes​

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. Tells the player that the bet failed.
400 to 499A final rejectionRejects the bet and stops. Tells the player that the bet failed.
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" or "error" 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.

Request signing​

Both directions use the same method, the same header, and the same shared secret. Prediction Markets gives you the secret before you go live.

Algorithm​

signature = HMAC-SHA256(raw_request_body, shared_secret)
header = hex_encode(signature)

Send the signature in the X-REQUEST-SIGN header. Send Content-Type: application/json with each request.

Sign the raw body

Sign the exact bytes of the body. If you build the JSON again before you sign it, 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.

Test vector​

Use these values to test your code:

  • Body: {"amount":"10.50"}
  • Secret: test-secret
  • Signature: 37f9186da8bef5457f94d56d1c76dc37f8c8854e35751cf7eb795da23d593329

For code examples in Node.js, see Sign the request body and Verify the signature. The Go example and the Python example in Wallet Endpoints use the same algorithm, but a different header name.

No replay protection​

This scheme has no timestamp and no nonce value. Compare it with the Standalone path, which sends X-Timestamp and X-Nonce.

The signature alone does not stop an attacker who sends a copy of a valid request a second time. Two controls limit the effect:

  1. Idempotency. A second copy of a request has the same id_provider. Your endpoints must recognize the repeated value and must not move the money a second time.
  2. Network controls, if you use them. Ask Prediction Markets for the addresses that it calls your wallet from. Accept wallet requests only from those addresses. Prediction Markets can also accept Launcher requests only from your addresses.

Do not use the signature as your only control.

Configuration​

Prediction Markets gives you these values before you go live:

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 X-REQUEST-SIGN, in both directions
Source IP addressesOptional. The addresses that Prediction Markets calls your wallet from. Ask for these if you filter by address.

The game identifier is prediction-markets. Send this value in the game field of a Launcher request. Prediction Markets sends the same value in the game_id field of each wallet request.

You give Prediction Markets these values:

ValueDescription
Wallet base URLThe host for the four provider_a8r paths
casino_id valuesThe casinos that use this game. Prediction Markets does not read this field, but it helps support.
Source IP addressesThe addresses that your platform calls the Launcher from

API Reference​

Full request and response schemas:

See Also​