Appearance
Free Rounds
Availability
Free rounds are an opt-in add-on — enabled per partner after a short joint test (see checklist below). Ask your TS contact to enable it for your stage brand before integrating. Until it is enabled for your brand, no free-round traffic will ever reach your wallet API.
What a free round is in Tap Markets
Tap Markets is not a slot: a round is a tap — the player picks a price cell and the market settles it seconds later. A free round is therefore:
an operator-funded tap — the player places a trade at a stake they did not pay. A win credits them real money; a loss costs them nothing.
Because there are no reels, two things a slot takes for granted are explicit fields you choose when awarding: the stake per round (you price the promo) and the instrument scope (or all instruments).
Not supported by design: retriggers (a round awarding more rounds), lines, gamble features. Expected promo cost per round ≈ stake × RTP — ask TS for the measured payout figures before pricing a campaign.
Flow at a glance
Everything rides the wallet callbacks you already implement — free rounds add one new call from you to TS (the grant) and a few extra fields on /opentrade / /closetrade.
1. Awarding free rounds (you → TS)
POST https://<your-tap-host>/api/partner/free-rounds
Signed with the same HMAC v2 scheme you already verify on wallet callbacks (see Message Integrity) — same shared secret, same X-Sig-Version: v2, X-Timestamp, X-Nonce, X-Signature headers, just in the other direction: you sign, TS verifies.
Request:
json
{
"userID": "alice",
"count": 10,
"stake": "1.00",
"currency": "USD",
"reference": "campaign-2026-08-abc123",
"validUntil": "2026-08-31T23:59:59Z"
}| Field | Required | Meaning |
|---|---|---|
userID | yes | your user id — the same one you return from /authenticate |
count | yes | number of free taps (1–10000) |
stake | yes | stake per free tap, in currency — you price the promo |
currency | yes | must match the player's wallet currency |
reference | recommended | your campaign reference — makes the grant idempotent: resending the same reference returns the same grant, never a duplicate. Safe to retry on timeout. |
validUntil | optional | ISO-8601 UTC expiry; omit for no expiry |
brand | optional | your brand key — only needed when calling from a host other than your tap host (the brand normally resolves from the request's Host header). Single-brand operators can always omit it. |
Response:
json
{ "status": "Ok", "freeRoundId": "fr-a1b2c3d4e5f60718", "count": 10, "stake": "1.00", "currency": "USD", "newPlayer": false }freeRoundId is minted by TS and identifies the grant on every subsequent wallet callback. Grants can be awarded before the player's first launch — in that case newPlayer is true.
Check newPlayer
userID must be your player id — the userId your /authenticate returns (the same value TS echoes back in /opentrade data[0].userID). If you grant to a player you believe has already played and the response says "newPlayer": true, the id didn't match anyone — the rounds are bound to a new empty account and your player will never see them.
Errors use the same status enum as the wallet callbacks (InvalidDigest, Unauthorized, plus InvalidRequest with a message for bad count/stake/expiry).
2. The free /opentrade — approve, never charge
Each free tap arrives as a normal /opentrade with two differences:
json
{
"token": "<walletToken>",
"amount": "0",
"currency": "USD",
"externalTradeType": "tap",
"externalTradeId": "12399",
"freeRoundId": "fr-a1b2c3d4e5f60718",
"data": [{
"userID": "alice",
"instrumentID": "30102",
"stake": "1.00",
"payoutRatio": "1.7",
"isFreeRound": "1",
"tStart": "2026-08-04T12:00:00.000Z",
"tEnd": "2026-08-04T12:00:02.000Z",
"pMin": "65430.00",
"pMax": "65432.00",
"currency": "USD"
}]
}- Top-level
amountis"0"— reserve nothing, deduct nothing.data[0].stakestill carries the promo's notional stake (for your reporting), but no money moves. freeRoundId+data[0].isFreeRound: "1"mark the round.- Respond exactly as usual:
{ "balance": "<unchanged>", "currency": "USD", "status": "Ok" }. - Idempotency by
externalTradeId, unchanged. - Answering
InsufficientFunds/Unauthorized-class rejections on a free open is read by TS as "the operator revoked this promo": the grant is cancelled and no further free rounds are offered. The round you rejected is already in flight on TS's side, so its/closetradewill still arrive — handle it normally (it moves at most the win amount; a loss closes with0).
3. The free /closetrade — where you pay the win
Settlement is a normal /closetrade with the free-round fields added:
json
{
"token": "<walletToken>",
"amount": "1.70",
"currency": "USD",
"externalTradeType": "tap",
"externalTradeId": "12399",
"freeRoundId": "fr-a1b2c3d4e5f60718",
"freeRoundsRemaining": 6,
"freeRoundCompleted": "0",
"data": [{ "tradeID": "12399", "userID": "alice", "stake": "1.0000000000",
"pnl": "0.70", "returnedAmount": "1.70", "isFreeRound": "1",
"instrumentID": "30102", "expiryPrice": "65431.50", "touchPrice": "65431.40",
"payout": "70", "rebate": "0" }]
}The "always update balance from amount" rule from Wallet / Trade Callbacks applies unchanged, and does the right thing automatically:
| Outcome | amount | Your action |
|---|---|---|
| Win | full return (stake × payoutRatio, e.g. 1.70) | credit it whole — the unpaid stake is not deducted (slot convention) |
| Loss | 0 | credit nothing; the round still closes — a 0-amount free close is normal, not an error |
| Cancelled | arrives as a cancellation, not a close | no-op, and the round goes back onto the grant |
Extra fields:
| Field | Meaning |
|---|---|
freeRoundsRemaining | rounds left on the grant after this one |
freeRoundCompleted | "1" on the grant's last round — the grant is then exhausted |
Values are snapshotted at settle time: a retried delivery repeats identical values under the identical externalTradeId.
Funding and reporting
- The operator funds free-round wins (your promo liability) — the same convention as slot free spins. Confirm this matches your accounting before go-live.
- Both callbacks carry
isFreeRound: "1", so you can split promo turnover from real-money turnover in your reports. TS excludes free-round volume from RTP/GGR statistics on its side the same way.
Expiry & lifecycle
| Situation | Behaviour |
|---|---|
| all rounds used | grant exhausted on the last close (freeRoundCompleted: "1") |
validUntil passes | unused rounds silently expire — no wire traffic |
you reject a free /opentrade | grant cancelled (promo revoked); the in-flight round still settles, no further free rounds |
| round opened just before expiry | always settles — expiry is checked at open only |
| grant currency ≠ wallet currency | free tap refused; re-award with the right currency |
Unused rounds are never converted to cash.
Test checklist
Run on stage with TS before enabling in production:
- Grant 3 rounds @ 1.00 → note
freeRoundId; resend the samereference→ samefreeRoundId, still 3 rounds. - Launch → banner shows 3 @ 1.00.
- Free tap →
/opentrade amount "0"received, player balance unchanged. - Win →
/closetrade amount "1.70"→ balance +1.70 exactly. - Loss →
/closetrade amount "0"→ balance unchanged, round consumed. - Last round →
freeRoundsRemaining: 0,freeRoundCompleted: "1". - Short-expiry grant → let it lapse → no wire traffic, rounds gone.
- Reject one free
/opentrade→ grant cancelled, remaining rounds gone.