@edge_protocol/bankroll-sdk
v1.9.17
Published
TypeScript SDK for Edge Protocol Bankroll - Build games without writing Solana programs
Maintainers
Readme
@edge_protocol/bankroll-sdk
TypeScript SDK for the Edge Protocol Bankroll program on Solana. Build casino-style games without deploying your own Solana program.
Installation
npm install @edge_protocol/bankroll-sdkPackage Exports
| Import path | Use in | Description |
|---|---|---|
| @edge_protocol/bankroll-sdk | Browser + Server | Core client, types, PDA helpers, wager ID generation |
| @edge_protocol/bankroll-sdk/server | Server only | NodeWallet, connection/keypair singletons, getSdkClient() |
| @edge_protocol/bankroll-sdk/routes | Server only | Ready-made Next.js route handlers for place-bet, settle-bet, pool-info |
Quick Start (Next.js Game)
A new game needs three things: environment variables, three API route files, and a game hook that uses BankrollApiClient.
1. Environment Variables
# Solana / Pool Configuration
NEXT_PUBLIC_RPC_ENDPOINT=https://api.devnet.solana.com
NEXT_PUBLIC_POOL_ADDRESS=<pool-pubkey>
NEXT_PUBLIC_GAME_CONFIG_ADDRESS=<game-config-pubkey>
NEXT_PUBLIC_USDC_MINT=<usdc-mint-pubkey>
NEXT_PUBLIC_FEE_TREASURY=<fee-treasury-pubkey>
GAME_KEYPAIR_SECRET=<base58-encoded-game-secret-key>
# Game Identity (required)
EDGE_GAME_NAME=<your-game-name> # e.g. "dice", "slots", "roulette"EDGE_GAME_NAME is required. This identifies your game to the Edge Protocol team for support and troubleshooting. Use a short, lowercase, unique name for your game (e.g. dice, slots, roulette).
If you're using EdgeBankrollClient directly instead of the route handlers, pass gameName in the constructor:
const client = new EdgeBankrollClient({
gameKeypair,
bankrollProgramId: program.programId,
program,
gameName: "my-game",
});2. API Routes
// src/app/api/place-bet/route.ts
import { handlePlaceBet } from "@edge_protocol/bankroll-sdk/routes";
export async function POST(request: Request) { return handlePlaceBet(request); }// src/app/api/settle-bet/route.ts
import { handleSettleBet } from "@edge_protocol/bankroll-sdk/routes";
export async function POST(request: Request) { return handleSettleBet(request); }// src/app/api/pool-info/route.ts
import { handlePoolInfo } from "@edge_protocol/bankroll-sdk/routes";
export async function GET(request: Request) { return handlePoolInfo(request); }3. Browser-Side Client
import { BankrollApiClient, generateWagerId } from "@edge_protocol/bankroll-sdk";
const bankroll = new BankrollApiClient();
// Fetch pool info (max bet, utilization, etc.)
const poolInfo = await bankroll.getMaxBet();
// Place a bet — returns a partially-signed transaction for the player's wallet to co-sign
const wagerId = generateWagerId("dice");
const tx = await bankroll.placeBet({
player: walletPublicKey,
amount: 1_000_000, // 1 USDC (6 decimals)
wagerId,
});
// Sign and send. Use sendAndConfirmIdempotent to avoid the "already been
// processed" error caused by React Strict Mode, double-clicks, or stale
// tx closures resubmitting the same signed transaction.
import { sendAndConfirmIdempotent } from "@edge_protocol/bankroll-sdk";
const signed = await wallet.signTransaction(tx);
const sig = await sendAndConfirmIdempotent(connection, signed);
// Settle — fully server-side, auto-batches if multiple settlements
await bankroll.settleBet({
player: walletPublicKey,
wagerId,
betAmount: 1_000_000,
payoutAmount: 2_000_000, // player won 2x
gameData: JSON.stringify({ result: "win", roll: 18 }),
});4. Local Dev with MockBankrollClient
Use MockBankrollClient to develop without a Solana connection. It has the same interface as BankrollApiClient.
import { BankrollApiClient, MockBankrollClient } from "@edge_protocol/bankroll-sdk";
const isDevnet = process.env.NEXT_PUBLIC_RPC_ENDPOINT?.includes("devnet");
const bankroll = isDevnet
? new BankrollApiClient()
: new MockBankrollClient({
reserveMultiplier: 3, // e.g. 3:1 max payout
poolTotalAssets: 10_000_000_000,
maxBetBps: 500, // 5% of pool
maxUtilizationBps: 8000, // 80%
});API Reference
Core (@edge_protocol/bankroll-sdk)
EdgeBankrollClient
Low-level client for direct on-chain interaction. Used server-side.
import { EdgeBankrollClient, createBankrollProgram } from "@edge_protocol/bankroll-sdk";
const program = createBankrollProgram(connection, wallet);
const client = new EdgeBankrollClient({
gameKeypair,
bankrollProgramId: program.programId,
program,
gameName: "my-game",
});Methods:
| Method | Description |
|---|---|
| placeBet(params) | Build, sign, and submit a place-bet transaction |
| settleBet(params) | Build, sign, and submit a settle-bet transaction |
| settleBets(params) | Settle multiple bets, auto-batching into transactions (max 2 per tx due to Solana size limit) |
| buildPlaceBetInstruction(params) | Build a place-bet instruction without signing (for partial-sign flows) |
| buildSettleBetInstruction(params) | Build a settle-bet instruction without signing (for batching) |
| getGameConfig() | Fetch the on-chain game config |
| getPool() | Fetch the pool account |
| getActiveBet(wagerId) | Get an active bet by wager ID (null if not found) |
| getMaxBet() | Calculate the maximum bet the pool can support for this game |
settleBets
Auto-batches settle instructions into multiple transactions (max 2 instructions per tx) to stay under Solana's 1232-byte transaction limit. Returns an array of transaction signatures.
const signatures = await client.settleBets({
settlements: [
{ player, playerTokenAccount, wagerId: "bj_1", betAmount: new BN(1000000), payoutAmount: new BN(2000000), gameData: '{"result":"win"}' },
{ player, playerTokenAccount, wagerId: "bj_2", betAmount: new BN(500000), payoutAmount: new BN(0), gameData: '{"result":"bust"}' },
{ player, playerTokenAccount, wagerId: "bj_3", betAmount: new BN(1000000), payoutAmount: new BN(1000000), gameData: '{"result":"push"}' },
],
payer: gameKeypair,
});
// signatures.length === 2 (first tx has 2 settlements, second has 1)getMaxBet
Returns MaxBetInfo with the maximum bet amount considering:
- Game config
max_bet_bps(% of pool) - Pool utilization and reserved assets
- Reserve multiplier calculated from payout tiers
const info = await client.getMaxBet();
// info.maxBet: BN — max bet in token smallest units
// info.reserveMultiplier: number — payout tier multiplier
// info.poolTotalAssets: BN
// info.poolReservedAssets: BN
// info.poolAvailableLiquidity: BN
// info.poolUtilizationBps: number
// info.isPaused: boolean
// info.isGameActive: booleanBankrollApiClient
Browser-side client that calls your game's API routes (/api/place-bet, /api/settle-bet, /api/pool-info).
| Method | Returns | Description |
|---|---|---|
| placeBet(params) | Transaction | Partially-signed transaction for the player's wallet |
| placeBets(player, bets[]) | Transaction | Multiple place-bet instructions in one transaction |
| settleBet(params) | string | Transaction signature |
| settleBets(player, settlements[]) | string | Last transaction signature |
| getMaxBet() | PoolInfo | Pool info (max bet, utilization, etc.) |
| generateWagerId(prefix) | string | Unique wager ID (max 32 chars) |
MockBankrollClient
Drop-in replacement for BankrollApiClient for local development. Tracks bets in memory with configurable pool parameters.
new MockBankrollClient({
reserveMultiplier?: number, // default: 1
poolTotalAssets?: number, // default: 10_000_000_000
maxBetBps?: number, // default: 500 (5%)
maxUtilizationBps?: number, // default: 8000 (80%)
})generateWagerId(prefix?)
Generate a unique wager ID (max 32 chars). Available as standalone function and as a method on both client classes.
import { generateWagerId } from "@edge_protocol/bankroll-sdk";
const id = generateWagerId("poker"); // e.g. "poker_1709654321_a1b2"createBankrollProgram(connection, wallet?)
Create an Anchor Program instance with the bundled IDL. No manual IDL loading needed.
sendAndConfirmIdempotent(connection, signedTx, opts?)
Idempotent send + confirm for a pre-signed transaction. Use this in any code path that calls connection.sendRawTransaction on a pre-signed tx — it eliminates the "Transaction simulation failed: This transaction has already been processed" error that surfaces when the same signed tx is submitted twice.
When duplicate sends happen:
- React Strict Mode double-invoke during dev
- Double-clicks before an
isLoadingflag flips - A
txreference captured in a stale closure - Wallet-adapter retries
- Hot-reload re-running an effect
How it works:
- Solana signatures are deterministic over
(blockhash + feePayer + instructions + signers), so a fully-signed tx already carries its final on-chain signature in its first-signer slot. - Pre-checks
getSignatureStatus(sig, { searchTransactionHistory: true })— if the tx already landed, returns the signature without re-sending. - Otherwise sends + confirms.
- If a concurrent send races us into
"already been processed"between pre-check and submit, recovers the deterministic signature and confirms it.
Drop-in replacement for the typical sign + send pattern:
import { sendAndConfirmIdempotent } from "@edge_protocol/bankroll-sdk";
// Before — vulnerable to duplicate submits:
const signed = await wallet.signTransaction(partialTx);
const sig = await connection.sendRawTransaction(signed.serialize());
await connection.confirmTransaction(sig, "confirmed");
// After — idempotent:
const signed = await wallet.signTransaction(partialTx);
const sig = await sendAndConfirmIdempotent(connection, signed, {
commitment: "confirmed", // optional, defaults to 'confirmed'
});Inside a React signAndSend hook:
const signAndSend = useCallback(
async (partialTx) => {
if (!partialTx) return "mock";
if (!signTransaction) throw new Error("Wallet does not support signing");
const signed = await signTransaction(partialTx);
return sendAndConfirmIdempotent(connection, signed);
},
[signTransaction, connection]
);Where to use it:
| Code path | Use? | Why |
|---|---|---|
| Client-side signAndSend in React hooks | Yes | Strict Mode, double-clicks, and stale closures cause the most dups here |
| Anywhere you call connection.sendRawTransaction(rawTx) with a pre-signed tx | Yes | One extra getSignatureStatus is cheap insurance |
| EdgeBankrollClient.placeBet / settleBet (server-side) | Not needed by default | They use Anchor's provider.sendAndConfirm, which signs internally and doesn't auto-retry. Wrap manually only if you have your own retry logic above. |
| BankrollApiClient.placeBet (browser → server route) | No | This just fetches a partial tx; the actual send happens in your signAndSend |
| Server route handlers (/api/place-bet, /api/settle-bet) | No (build) / Yes (/api/settle-bet if you wrap retries) | /api/place-bet only builds; /api/settle-bet calls EdgeBankrollClient.settleBets |
Options:
sendAndConfirmIdempotent(connection, signedTx, {
commitment: "finalized", // confirmation level — defaults to 'confirmed'
});Throws if the tx isn't signed (no first-signer signature to derive from).
Don't disable preflight. The default preflight simulation is what surfaces program errors (insufficient funds, account constraint failures, decoded Anchor errors) before the tx is broadcast — and Sight uses those simulation logs for the failed-tx enrichment path. Pass
sendOptionsonly if you understand what you're turning off.
PDA Helpers
import {
findPoolPda,
findGameConfigPda,
findActiveBetPda,
findBetEscrowPda,
findSettledBetPda,
findVaultPda,
findPlayerAccountPda,
findPlayerEscrowTokenPda,
findMetadataPda,
} from "@edge_protocol/bankroll-sdk";BankrollIDL
The bundled Bankroll IDL JSON. Import directly if needed for custom Anchor usage.
Server (@edge_protocol/bankroll-sdk/server)
Server-side utilities for Next.js API routes. Uses module-level singletons for connection, keypair, and program instances.
| Export | Description |
|---|---|
| NodeWallet | Minimal wallet adapter that works with Next.js ESM bundling |
| decodeKeypair(base58) | Decode a base58-encoded secret key into a Keypair |
| getConnection() | Singleton Connection from NEXT_PUBLIC_RPC_ENDPOINT |
| getGameKeypair() | Singleton Keypair from GAME_KEYPAIR_SECRET |
| getProgram() | Singleton Anchor Program with bundled IDL |
| getPoolAddress() | PublicKey from NEXT_PUBLIC_POOL_ADDRESS |
| getGameConfigAddress() | PublicKey from NEXT_PUBLIC_GAME_CONFIG_ADDRESS |
| getAssetMint() | PublicKey from NEXT_PUBLIC_USDC_MINT |
| getFeeTreasury() | PublicKey from NEXT_PUBLIC_FEE_TREASURY |
| getSdkClient() | Singleton EdgeBankrollClient ready to use |
Routes (@edge_protocol/bankroll-sdk/routes)
Pre-built route handlers using standard web Request/Response (no Next.js dependency). Wire them directly into Next.js App Router routes.
handlePlaceBet(request: Request): Promise<Response>
POST /api/place-bet
Request body (single bet):
{ "player": "<pubkey>", "wagerId": "dice_123", "amount": 1000000 }Request body (multiple bets):
{ "player": "<pubkey>", "bets": [{ "wagerId": "bj_1", "amount": 1000000 }, { "wagerId": "bj_2", "amount": 500000 }] }Returns { "transaction": "<base64>" } — a partially-signed transaction for the player to co-sign.
handleSettleBet(request: Request): Promise<Response>
POST /api/settle-bet
Request body (single settlement):
{ "player": "<pubkey>", "wagerId": "dice_123", "betAmount": 1000000, "payoutAmount": 2000000, "gameData": "{\"result\":\"win\"}" }Request body (batch):
{ "player": "<pubkey>", "settlements": [{ "wagerId": "bj_1", "betAmount": 1000000, "payoutAmount": 0, "gameData": "{}" }] }Returns { "signature": "<tx-sig>", "signatures": ["<tx-sig-1>", "<tx-sig-2>"] }. Auto-batches into multiple transactions if needed.
handlePoolInfo(request: Request): Promise<Response>
GET /api/pool-info
Returns:
{
"maxBet": 180000,
"reserveMultiplier": 3,
"totalAssets": 1805155080000,
"reservedAssets": 0,
"availableLiquidity": 1805155080000,
"utilizationBps": 0,
"isPaused": false,
"isGameActive": true
}Architecture
Browser (React) Server (Next.js API Routes)
┌─────────────────────┐ ┌──────────────────────────┐
│ BankrollApiClient │─── /api/place-bet ──▶│ handlePlaceBet() │
│ or MockBankrollClient│─── /api/settle-bet ─▶│ handleSettleBet() │
│ │─── /api/pool-info ──▶│ handlePoolInfo() │
└─────────────────────┘ └────────────┬─────────────┘
│
│ EdgeBankrollClient
│ (signs with game keypair)
▼
┌──────────────────────────┐
│ Edge Bankroll Program │
│ (on-chain Solana) │
│ - Manages reserves │
│ - Enforces house edge │
│ - Handles payouts │
└────────────┬─────────────┘
│
▼
┌──────────────────────────┐
│ Liquidity Pool │
│ - LP token holders │
│ - Insurance fund │
└──────────────────────────┘Place bet flow: Browser builds request → server builds transaction and partial-signs with game keypair → browser receives transaction → player's wallet co-signs and submits.
Settle bet flow: Browser sends settlement data → server builds + fully signs + submits transaction(s) via settleBets() auto-batching.
Security
- Game keypair is your authentication credential. Store it in environment variables, never expose client-side.
- Place-bet transactions are partially signed by the game keypair server-side. The player's wallet signs and submits.
- Settle-bet transactions are fully signed server-side by the game keypair. Only your server can settle bets for your game.
License
MIT
