Skip to content

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"
}
FieldRequiredMeaning
userIDyesyour user id — the same one you return from /authenticate
countyesnumber of free taps (1–10000)
stakeyesstake per free tap, in currency — you price the promo
currencyyesmust match the player's wallet currency
referencerecommendedyour campaign reference — makes the grant idempotent: resending the same reference returns the same grant, never a duplicate. Safe to retry on timeout.
validUntiloptionalISO-8601 UTC expiry; omit for no expiry
brandoptionalyour 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 amount is "0" — reserve nothing, deduct nothing. data[0].stake still 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 /closetrade will still arrive — handle it normally (it moves at most the win amount; a loss closes with 0).

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:

OutcomeamountYour action
Winfull return (stake × payoutRatio, e.g. 1.70)credit it whole — the unpaid stake is not deducted (slot convention)
Loss0credit nothing; the round still closes — a 0-amount free close is normal, not an error
Cancelledarrives as a cancellation, not a closeno-op, and the round goes back onto the grant

Extra fields:

FieldMeaning
freeRoundsRemainingrounds 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

SituationBehaviour
all rounds usedgrant exhausted on the last close (freeRoundCompleted: "1")
validUntil passesunused rounds silently expire — no wire traffic
you reject a free /opentradegrant cancelled (promo revoked); the in-flight round still settles, no further free rounds
round opened just before expiryalways settles — expiry is checked at open only
grant currency ≠ wallet currencyfree 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:

  1. Grant 3 rounds @ 1.00 → note freeRoundId; resend the same reference → same freeRoundId, still 3 rounds.
  2. Launch → banner shows 3 @ 1.00.
  3. Free tap → /opentrade amount "0" received, player balance unchanged.
  4. Win → /closetrade amount "1.70" → balance +1.70 exactly.
  5. Loss → /closetrade amount "0" → balance unchanged, round consumed.
  6. Last round → freeRoundsRemaining: 0, freeRoundCompleted: "1".
  7. Short-expiry grant → let it lapse → no wire traffic, rounds gone.
  8. Reject one free /opentrade → grant cancelled, remaining rounds gone.