Batch Deposit
Credit multiple player balances in a single request for high-volume games
POST {api_base_url}/deposit/batch
Called to credit multiple players' balances in a single request — used for high-volume games like Keno where many bets close simultaneously.
This endpoint is optional and used for performance optimization. If not implemented, Phoenix Games will continue using individual /deposit calls for each bet.
When to Use
Batch deposit is designed for games where many bets (hundreds to thousands) close at the same time, such as:
- Keno - All tickets close when the round ends (can be 1000+ bets)
- Bingo - All cards resolve simultaneously
- Crash - All active bets cash out when the game crashes
Benefits:
- Reduces HTTP request overhead from thousands of individual calls to ~10 batch requests
- Lower network latency and improved throughput
- Easier database transaction management with batch updates
Enabling Batch Deposit
To enable batch deposit for your operator integration:
- Implement the
/deposit/batchendpoint - Contact Phoenix Games support to enable the
supports_batch_depositflag for your operator
Once enabled, Phoenix Games will automatically route bet deposits to the batch endpoint instead of individual calls.
Request Fields
| Field | Type | Description |
|---|---|---|
bets | array of BetDepositItem | List of bets to credit |
BetDepositItem Structure
| Field | Type | Description |
|---|---|---|
player_id | string | Unique player ID |
bet_id | string | Unique bet ID |
amount | integer | Total amount being deposited (in cents) |
game | string | Game ID |
instance_id | string | Game instance ID |
round_id | string | ID of the round |
wager | integer | Amount wagered (in cents) |
won | integer | Amount won (in cents) |
tx_id | string | Unique transaction ID |
Request Example
{
"bets": [
{
"player_id": "player_123",
"bet_id": "bet_abc",
"amount": 15000,
"game": "keno",
"instance_id": "instance_xyz",
"round_id": "round_789",
"wager": 10000,
"won": 15000,
"tx_id": "deposit:bet:bet_abc"
},
{
"player_id": "player_456",
"bet_id": "bet_def",
"amount": 5000,
"game": "keno",
"instance_id": "instance_xyz",
"round_id": "round_789",
"wager": 10000,
"won": 5000,
"tx_id": "deposit:bet:bet_def"
}
// ... up to 1000 bets per batch
]
}Response Format
Success Response
Return the updated balance for each unique player in the batch:
{
"type": "SUCCESS",
"balances": [
{
"player_id": "player_123",
"balance": 25000.50
},
{
"player_id": "player_456",
"balance": 12500.75
}
],
"timestamp": 1712401234567
}| Field | Type | Description |
|---|---|---|
type | string | Always "SUCCESS" for successful operations |
balances | array of PlayerBalance | Updated balances for each player |
timestamp | integer | Timestamp in milliseconds |
PlayerBalance Structure
| Field | Type | Description |
|---|---|---|
player_id | string | Player ID |
balance | float | Updated player balance after all deposits |
Error Response
If any bet in the batch fails validation, reject the entire batch:
{
"type": "ERROR",
"code": "BATCH_VALIDATION_FAILED"
}| Field | Type | Description |
|---|---|---|
type | string | Always "ERROR" for failed operations |
code | string | Error code explaining failure reason |
Error Codes
BATCH_VALIDATION_FAILED- One or more bets in the batch failed validationINVALID_REQUEST- Request format is invalidPLAYER_NOT_FOUND- One or more players don't existINSUFFICIENT_BALANCE- Internal error (shouldn't happen for deposits)
Implementation Guidelines
1. Transaction Handling
Process the entire batch in a single database transaction for atomicity:
async function processBatchDeposit(request: BatchDepositRequest) {
return await db.transaction(async (tx) => {
// Process all bets in the batch
for (const bet of request.bets) {
// Check idempotency
if (await alreadyProcessed(tx, bet.tx_id)) {
continue;
}
// Credit player balance
await tx.execute(
'UPDATE players SET balance = balance + ? WHERE id = ?',
[bet.amount / 100, bet.player_id]
);
// Mark transaction as processed
await tx.execute(
'INSERT INTO processed_transactions (tx_id, bet_id) VALUES (?, ?)',
[bet.tx_id, bet.bet_id]
);
}
// Fetch updated balances for all unique players
const playerIds = [...new Set(request.bets.map(b => b.player_id))];
const balances = await tx.query(
'SELECT id, balance FROM players WHERE id IN (?)',
[playerIds]
);
return {
type: 'SUCCESS',
balances: balances.map(p => ({
player_id: p.id,
balance: p.balance
})),
timestamp: Date.now()
};
});
}2. Idempotency
CRITICAL: Check each tx_id for idempotency to prevent duplicate credits:
async function alreadyProcessed(tx, txId: string): Promise<boolean> {
const result = await tx.query(
'SELECT 1 FROM processed_transactions WHERE tx_id = ? LIMIT 1',
[txId]
);
return result.length > 0;
}3. Validation
Validate the entire batch before processing any bets:
- Verify all
player_ids exist - Check all
tx_ids are unique within the batch - Ensure amounts are positive
- Validate
round_idmatches across all bets (they're from the same round)
If validation fails, return an error without processing any bets.
4. Performance Optimization
For large batches (500-1000 bets):
- Use batch INSERT/UPDATE queries instead of individual queries
- Consider using prepared statements with batch execution
- Use indexes on
tx_idandplayer_idcolumns - Minimize database round trips
Example batch update:
-- Batch update balances using VALUES clause
UPDATE players SET balance = balance + updates.amount
FROM (VALUES
('player_123', 150.00),
('player_456', 50.00)
) AS updates(player_id, amount)
WHERE players.id = updates.player_id;Batch Behavior
Batch Sizing
Phoenix Games sends batches based on:
- Max size: 1000 bets per batch
- Max wait: 50ms from first bet in the batch
This means:
- If 1000 bets close simultaneously → 1 batch of 1000 bets
- If 8000 bets close simultaneously → 8 batches of 1000 bets each
- If only 200 bets close → 1 batch of 200 bets after 50ms
Retry Behavior
If a batch deposit fails:
- Phoenix Games retries the entire batch up to 10 times with exponential backoff
- Same retry logic as individual
/depositcalls - All bets in the batch will be NACK'd and redelivered if max retries exceeded
Security
Batch requests are signed the same way as individual requests:
- Request body is signed with RSA SHA256
- Signature included in
signatureheader - Verify signature covers the entire batch body
Migration Guide
Step 1: Implement the Endpoint
Add the /deposit/batch endpoint to your API while keeping the existing /deposit endpoint.
Step 2: Test Thoroughly
Test with various batch sizes:
- Small batches (1-10 bets)
- Medium batches (100-500 bets)
- Large batches (1000 bets)
- Ensure idempotency works correctly
- Verify transaction rollback on errors
Step 3: Enable in Production
Contact Phoenix Games support to enable the supports_batch_deposit flag. We'll enable it for your operator after reviewing your implementation.
Step 4: Monitor
After enabling:
- Monitor batch processing times
- Check for any failed batches in logs
- Verify player balance accuracy
- Compare batch vs individual endpoint performance
Backward Compatibility
If batch deposit is not implemented or disabled:
- Phoenix Games automatically falls back to individual
/depositcalls - No configuration changes needed
- Existing integrations continue to work unchanged
All-or-Nothing Processing
If the batch deposit endpoint returns an error, the entire batch is rejected and retried. Ensure your implementation processes all bets atomically — either all succeed or all fail.
Example Implementations
import express from 'express';
import { verifySignature } from './security';
app.post('/deposit/batch', async (req, res) => {
// Verify signature
if (!verifySignature(req.body, req.headers.signature)) {
return res.status(401).json({ type: 'ERROR', code: 'INVALID_SIGNATURE' });
}
const { bets } = req.body;
try {
// Process in transaction
const result = await db.transaction(async (tx) => {
const playerBalances = new Map();
for (const bet of bets) {
// Check idempotency
const exists = await tx.query(
'SELECT 1 FROM transactions WHERE tx_id = ?',
[bet.tx_id]
);
if (exists.length > 0) continue;
// Update balance
await tx.execute(
'UPDATE players SET balance = balance + ? WHERE id = ?',
[bet.amount / 100, bet.player_id]
);
// Record transaction
await tx.execute(
'INSERT INTO transactions (tx_id, bet_id, player_id, amount) VALUES (?, ?, ?, ?)',
[bet.tx_id, bet.bet_id, bet.player_id, bet.amount]
);
playerBalances.set(bet.player_id, null); // Mark for balance fetch
}
// Fetch updated balances
const playerIds = Array.from(playerBalances.keys());
const balances = await tx.query(
'SELECT id, balance FROM players WHERE id IN (?)',
[playerIds]
);
return balances.map(p => ({
player_id: p.id,
balance: p.balance
}));
});
res.json({
type: 'SUCCESS',
balances: result,
timestamp: Date.now()
});
} catch (error) {
console.error('Batch deposit error:', error);
res.status(500).json({
type: 'ERROR',
code: 'BATCH_PROCESSING_FAILED'
});
}
});from flask import Flask, request, jsonify
from sqlalchemy import create_engine
from contextlib import contextmanager
@app.route('/deposit/batch', methods=['POST'])
def batch_deposit():
# Verify signature
if not verify_signature(request.get_json(), request.headers.get('signature')):
return jsonify({'type': 'ERROR', 'code': 'INVALID_SIGNATURE'}), 401
data = request.get_json()
bets = data['bets']
try:
with transaction() as tx:
player_ids = set()
for bet in bets:
# Check idempotency
if tx.execute(
"SELECT 1 FROM transactions WHERE tx_id = %s",
(bet['tx_id'],)
).fetchone():
continue
# Update balance
tx.execute(
"UPDATE players SET balance = balance + %s WHERE id = %s",
(bet['amount'] / 100, bet['player_id'])
)
# Record transaction
tx.execute(
"INSERT INTO transactions (tx_id, bet_id, player_id, amount) VALUES (%s, %s, %s, %s)",
(bet['tx_id'], bet['bet_id'], bet['player_id'], bet['amount'])
)
player_ids.add(bet['player_id'])
# Fetch updated balances
balances = tx.execute(
"SELECT id, balance FROM players WHERE id = ANY(%s)",
(list(player_ids),)
).fetchall()
return jsonify({
'type': 'SUCCESS',
'balances': [
{'player_id': row[0], 'balance': float(row[1])}
for row in balances
],
'timestamp': int(time.time() * 1000)
})
except Exception as e:
print(f"Batch deposit error: {e}")
return jsonify({
'type': 'ERROR',
'code': 'BATCH_PROCESSING_FAILED'
}), 500<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
Route::post('/deposit/batch', function (Request $request) {
// Verify signature
if (!verifySignature($request->all(), $request->header('signature'))) {
return response()->json([
'type' => 'ERROR',
'code' => 'INVALID_SIGNATURE'
], 401);
}
$bets = $request->input('bets');
try {
$balances = DB::transaction(function () use ($bets) {
$playerIds = [];
foreach ($bets as $bet) {
// Check idempotency
$exists = DB::table('transactions')
->where('tx_id', $bet['tx_id'])
->exists();
if ($exists) continue;
// Update balance
DB::table('players')
->where('id', $bet['player_id'])
->increment('balance', $bet['amount'] / 100);
// Record transaction
DB::table('transactions')->insert([
'tx_id' => $bet['tx_id'],
'bet_id' => $bet['bet_id'],
'player_id' => $bet['player_id'],
'amount' => $bet['amount']
]);
$playerIds[] = $bet['player_id'];
}
// Fetch updated balances
$playerIds = array_unique($playerIds);
return DB::table('players')
->whereIn('id', $playerIds)
->get(['id', 'balance'])
->map(function ($player) {
return [
'player_id' => $player->id,
'balance' => (float) $player->balance
];
})->toArray();
});
return response()->json([
'type' => 'SUCCESS',
'balances' => $balances,
'timestamp' => now()->timestamp * 1000
]);
} catch (\Exception $e) {
Log::error('Batch deposit error: ' . $e->getMessage());
return response()->json([
'type' => 'ERROR',
'code' => 'BATCH_PROCESSING_FAILED'
], 500);
}
});
?>Testing Recommendations
- Idempotency Test: Send the same batch twice, verify balances only credited once
- Partial Duplicate Test: Include some already-processed
tx_ids in a batch - Multiple Players Test: Batch with bets from different players
- Single Player Test: All bets in batch from one player (ensure correct final balance)
- Error Recovery Test: Force a database error mid-batch, verify rollback
- Large Batch Test: Send 1000 bets, verify all processed within reasonable time
- Signature Validation Test: Send batch with invalid signature, verify rejection
Troubleshooting
High Latency
If batch processing is slow:
- Use batch UPDATE queries instead of individual updates
- Add database indexes on
tx_idandplayer_id - Consider connection pooling
- Profile your database queries
Memory Issues
For very large batches:
- Process in smaller chunks within the transaction
- Stream balance fetches instead of loading all at once
- Limit batch size via configuration
Partial Failures
If some bets fail mid-batch:
- Ensure entire transaction rolls back
- Log the specific error for debugging
- Return generic error to Phoenix Games (batch will retry)