AI prompt for your developers

Paste the block below into your AI coding assistant (Claude, ChatGPT, Cursor, Copilot) together with the name of your backend stack. It contains everything the assistant needs to build the integration. Your keys stay out of it: the assistant reads them from environment variables.

You are integrating our online casino with the Reelgate game aggregator. Build two parts in our backend.

PART 1: OPERATOR API CLIENT (we call Reelgate)
Base URL https://reelgate.code-matrix.tech. Credentials come from the env vars REELGATE_KEY and REELGATE_SECRET.
Sign EVERY request with these headers:
  X-Reelgate-Key: REELGATE_KEY
  X-Reelgate-Timestamp: unix seconds
  X-Reelgate-Signature: lowercase hex HMAC-SHA256(REELGATE_SECRET, ts + "\n" + METHOD + "\n" + pathWithQuery + "\n" + rawBody)
pathWithQuery is the target exactly as sent, starting with /api/v1, with any percent-encoding kept as sent. A GET has an empty rawBody.
Sign the exact bytes you send: serialize the JSON once and reuse that string as the body.
Errors come back as {"error":{"code","message"}} with an HTTP status. Map them to clear messages for the player.
Endpoints:
  GET  /api/v1/ping                       -> real_play true/false and refusal reason
  GET  /api/v1/games?provider=&q=&type=&demo=true&limit=200&cursor=   (page with "next" until it is null)
  GET  /api/v1/games/:id
  POST /api/v1/sessions {player_id, game_id, currency, mode:"real"|"demo", language, device:"desktop"|"mobile",
                         exit_url, cashier_url, nickname, country, player_ip}
       -> 201 {session:{token, url, ...}}; load session.url in an iframe or a new tab
  POST /api/v1/sessions/:token/close
  GET  /api/v1/transactions?player_id=&round_id=&from=&to=&cursor=
  GET  /api/v1/rounds/:round_id
  Free spins (optional):
  GET  /api/v1/freespins/options?currency=&game_ids=1,2  -> bet_steps, spin_counts, spins_max, days_max
  POST /api/v1/freespins {reference, player_id, game_ids, spins, bet (one of bet_steps), currency, valid_days}
       idempotent by reference; GET /api/v1/freespins/:reference for spins_used and win_total
  POST /api/v1/campaigns {reference, name, game_ids, spins, bet, currency, valid_days, players:[...]}
Tasks:
  - A job that syncs the game catalogue every hour into our games table (id, name, provider, studio, type, thumbnail_url,
    has_demo, currencies, languages). Only show a game in a currency listed in its "currencies".
  - A "launch game" endpoint for our frontend that calls POST /sessions with the logged-in player and returns the url.
    CURRENCY_NOT_SUPPORTED: tell the player the game is not available in their currency.
    KYB_NOT_APPROVED / OUT_OF_CREDIT: real play is closed; offer demo mode instead.

PART 2: SEAMLESS WALLET ENDPOINT (Reelgate calls us)
Expose ONE HTTPS POST endpoint, for example /reelgate/wallet. Reelgate sends JSON with an "action" field.
Verify every call BEFORE parsing JSON:
  expected = hex HMAC-SHA256(REELGATE_SECRET, X-Reelgate-Timestamp + "." + rawBody)
  Compare it with X-Reelgate-Signature in constant time. Reject if the timestamp is more than 300 s off.
  Also check that X-Reelgate-Key equals REELGATE_KEY.
Body fields: action, brand, player_id, currency, session_token, game_id, provider, transaction_id, amount (decimal string),
  round_id, round_closed, ref_transaction_id (rollback), promo (win), prize_type / prize_campaign / prize_funded_by (win),
  retry, request_id (balance).
Answer HTTP 200 {"status":"ok","balance":"<balance after, decimal string>"}
  or HTTP 200 {"status":"error","code":"<CODE>","message":"...","balance":"<current>"}.
Actions:
  balance  -> return the balance in `currency`.
  bet      -> debit `amount`. If the balance is too low, answer code INSUFFICIENT_FUNDS and book nothing.
  win      -> credit `amount` (may be 0). Never refuse a valid win.
              promo=true means a free-spin win: credit it the same way, but book it as a promo win in our reports.
              prize_type set (tournament, prize_drop, mission, cash_drop, ...) means a prize from a provider's own
              campaign (prize_campaign = its name, may be null): credit it the same way, book it as a provider prize.
  rollback -> refund the bet whose transaction_id = ref_transaction_id. If that bet was never booked, answer
              code TRANSACTION_NOT_FOUND. If it was already refunded, answer ok without moving money.
  adjust   -> add `amount`, which is usually NEGATIVE (the provider took back a win). Apply it even below zero.
IDEMPOTENCY (critical): store every transaction_id. If the same transaction_id arrives again, do NOT move money again;
  answer ok with the current balance. Reelgate re-sends unanswered wins and rollbacks with the same id.
Do the balance change and the transaction_id insert in ONE database transaction, with a unique index on
  (brand, transaction_id) and a row lock on the player balance, so two parallel calls cannot double-book.
Use exact decimal math (DECIMAL columns or integer minor units). Never use floats for money.
Answer within 2 seconds. Log every call with transaction_id, action, amount and the resulting balance.
Give each currency its own balance per player.

Write tests for:
  - a signature check that fails on one changed byte;
  - a repeated bet and a repeated win that each book only once;
  - a bet refused with INSUFFICIENT_FUNDS;
  - a rollback of a missing bet answering TRANSACTION_NOT_FOUND;
  - a rollback sent twice that refunds once;
  - a negative adjust.
Ask me for our stack, our database schema for players and balances, and where to mount the endpoint.

When the code is ready:

  1. Put the wallet URL into the portal (Brands → Wallet URL).
  2. Press Test wallet.
  3. Open a demo session from your own frontend.
  4. Open a real session once KYB is approved.