> For the complete documentation index, see [llms.txt](https://docs.fast.poker/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.fast.poker/building-on-the-protocol/deriving-pools.md).

# Deriving SNG pools

> Verified against the public fastpoker IDL (PokerXYdXL2SKNnfGbv1WE7vJHipTpNsfZbZeVvoJLn) released 2026-07-17.

**Capability: Wallet + RPC only**

> Beta software on mainnet. Examples can fail. Test with small amounts.

## Purpose

Fast Poker runs Sit-and-Go (SNG) games out of fixed on-chain pools. There are exactly 21 pools: 3 game types times 7 tiers. Each pool is a program-derived account (PDA) at a deterministic address, so you do not need an index or an API to find them. You derive all 21 addresses from constants, read them in one or two RPC calls, and decode the queue and economics from raw bytes.

This page shows how to derive every pool address, read all 21 accounts, and decode the fields a lobby needs: tier economics, how many players are waiting, and whether a match is already in progress.

The same `(game_type, tier)` bytes derive `["pool_idle", game_type, tier]`. Read that 83-byte `PoolIdle` with global `["emission_ctrl"]` when showing the cell's governed emission and idle-revival state. The pool itself does not store the live demand multiplier.

## When to use / who signs

Use this when you build a lobby, a queue-depth widget, an availability monitor, or any read surface that lists SNG pools. No transaction is sent and nothing is signed. You need a wallet pubkey only if you go on to join a pool, which is covered in [joining-a-sng.md](/building-on-the-protocol/joining-a-sng.md).

## Inputs and constants

### The 21 pools

A pool is keyed by `(game_type, tier)`. Both are single bytes.

GameType values (Table game-type discriminant):

| Value | Variant         | Players per table     |
| ----- | --------------- | --------------------- |
| 0     | SitAndGoHeadsUp | 2                     |
| 1     | SitAndGo6Max    | 6                     |
| 2     | SitAndGo9Max    | 9                     |
| 3     | CashGame        | n/a (not an SNG pool) |

Only game types 0, 1, and 2 have SNG pools. Game type 3 is cash and has no pool.

SNG tiers (the `SnGTier` enum). The total buy-in splits 90% to the prize pool and 10% to the fee. Values are mainnet (`TIER_SCALE = 1`):

| Tier byte | Enum name | Public name | Total buy-in (SOL) | Prize (SOL) | Fee (SOL) |
| --------- | --------- | ----------- | ------------------ | ----------- | --------- |
| 0         | Micro     | Copper      | 0.05               | 0.045       | 0.005     |
| 1         | Bronze    | Bronze      | 0.10               | 0.09        | 0.01      |
| 2         | Silver    | Silver      | 0.25               | 0.225       | 0.025     |
| 3         | Gold      | Gold        | 0.50               | 0.45        | 0.05      |
| 4         | Platinum  | Platinum    | 1.00               | 0.90        | 0.10      |
| 5         | Diamond   | Diamond     | 2.00               | 1.80        | 0.20      |
| 6         | Black     | Black       | 5.00               | 4.50        | 0.50      |

The discriminant-0 enum name in the program is `Micro`. The public display name is Copper. The table above is the canonical source for both. Read `entry_amount` and `fee_amount` from each pool account instead of hard-coding lamports, so your client tracks any future tier change.

For 6-max/9-max, pool selection also commits the player to Flat Bounty points, scheduled duels, the 50/50 SOL split, and maturity. Native HU remains classic. Render that disclosure before sending the join transaction.

### PDA seeds

The pool PDA derives against the FastPoker program ID:

```
seeds = ["sng_pool", game_type: u8, tier: u8]
```

`game_type` and `tier` are appended as single bytes, not multi-byte integers. The program IDs and seed strings used here match the shared helper in [setup.md](/building-on-the-protocol/setup.md); see that page for the full constant list.

### SngPool account layout

The account is 150 bytes. Fields are little-endian. Offsets are fixed by the on-chain `sng_pool.rs` layout and are stable for byte-level reads:

| Field              | Offset | Type      | Meaning                           |
| ------------------ | ------ | --------- | --------------------------------- |
| (discriminator)    | 0      | \[u8; 8]  | `SHA256("account:SngPool")[0..8]` |
| game\_type         | 8      | u8        | 0=HU, 1=6max, 2=9max              |
| tier               | 9      | u8        | 0=Copper .. 6=Black               |
| max\_players       | 10     | u8        | 2, 6, or 9                        |
| entry\_amount      | 11     | u64       | prize-pool lamports per seat      |
| fee\_amount        | 19     | u64       | fee lamports per seat             |
| waiting\_count     | 27     | u32       | players queued across all pages   |
| active\_match\_set | 83     | u8 (bool) | 1 when a match is in progress     |

`waiting_count` is the live queue depth. `active_match_set` tells you whether the pool is currently busy preparing or seating a match. The 32 bytes preceding `active_match_set` (offset 51) hold the active SngMatch PDA, valid only when `active_match_set` is 1. For the full account model and how SngPool relates to queue pages and matches, see [../04-architecture/state-accounts.md](/architecture/state-accounts.md).

## Steps

1. Derive all 21 pool PDAs by looping `game_type` over 0..2 and `tier` over 0..6 with the `["sng_pool", game_type, tier]` seeds.
2. Read the accounts. `getMultipleAccountsInfo` is one round trip, but some public RPC endpoints reject batches larger than 10 accounts, so chunk the 21 addresses into groups of 10 or fewer.
3. For each returned account, confirm the data length is at least 150 bytes, then decode `entry_amount`, `fee_amount`, `waiting_count`, and `active_match_set` at the offsets above.
4. A `null` slot means the pool is not initialized on this cluster yet. Treat it as an empty, joinable-once-created pool rather than an error.

For the general account-reading pattern (commitment, chunking, null handling), see [reading-accounts.md](/building-on-the-protocol/reading-accounts.md).

## Example

This example imports the shared helper from [setup.md](/building-on-the-protocol/setup.md), derives all 21 pools, reads them in chunks of 10, and prints queue depth and economics.

```ts
import { Connection, PublicKey } from '@solana/web3.js';
import {
  FASTPOKER_PROGRAM_ID,
  getSngPoolPda,
} from './setup'; // shared helper from setup.md

const GAME_TYPES = [
  { id: 0, label: 'HeadsUp' },
  { id: 1, label: '6Max' },
  { id: 2, label: '9Max' },
];

const TIERS = [
  'Copper', 'Bronze', 'Silver', 'Gold', 'Platinum', 'Diamond', 'Black',
];

// SngPool field offsets (sng_pool.rs, 150-byte account, little-endian).
const OFF_ENTRY_AMOUNT = 11; // u64
const OFF_FEE_AMOUNT = 19; // u64
const OFF_WAITING_COUNT = 27; // u32
const OFF_ACTIVE_MATCH_SET = 83; // u8 bool
const SNG_POOL_SIZE = 150;

function chunk<T>(items: T[], size: number): T[][] {
  const out: T[][] = [];
  for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
  return out;
}

function decodeSngPool(data: Buffer) {
  return {
    entryLamports: data.readBigUInt64LE(OFF_ENTRY_AMOUNT),
    feeLamports: data.readBigUInt64LE(OFF_FEE_AMOUNT),
    waitingCount: data.readUInt32LE(OFF_WAITING_COUNT),
    activeMatch: data.readUInt8(OFF_ACTIVE_MATCH_SET) === 1,
  };
}

async function readAllSngPools(connection: Connection) {
  // 1) Derive all 21 PDAs (3 game types x 7 tiers).
  const pools = GAME_TYPES.flatMap((gt) =>
    TIERS.map((tierName, tier) => {
      const [pda] = getSngPoolPda(gt.id, tier);
      return { gameType: gt.label, tierName, pda };
    }),
  );

  // 2) Read in chunks of <=10 (some public RPCs reject larger batches).
  const infos: (Buffer | null)[] = [];
  for (const group of chunk(pools, 10)) {
    const res = await connection.getMultipleAccountsInfo(
      group.map((p) => p.pda),
      'confirmed',
    );
    for (const acc of res) infos.push(acc ? acc.data : null);
  }

  // 3) Decode each pool after a length check.
  return pools.map((p, i) => {
    const data = infos[i];
    if (!data || data.length < SNG_POOL_SIZE) {
      return { ...p, initialized: false };
    }
    const decoded = decodeSngPool(data);
    return {
      ...p,
      initialized: true,
      entrySol: Number(decoded.entryLamports) / 1e9,
      feeSol: Number(decoded.feeLamports) / 1e9,
      waitingCount: decoded.waitingCount,
      matchInProgress: decoded.activeMatch,
    };
  });
}

async function main() {
  const connection = new Connection(
    process.env.RPC_URL ?? 'https://api.mainnet-beta.solana.com',
    'confirmed',
  );
  const rows = await readAllSngPools(connection);
  for (const r of rows) {
    if (!r.initialized) {
      console.log(`${r.gameType} ${r.tierName}: not initialized`);
      continue;
    }
    const buyin = (r.entrySol + r.feeSol).toFixed(3);
    console.log(
      `${r.gameType} ${r.tierName}: buy-in ${buyin} SOL ` +
        `(prize ${r.entrySol}, fee ${r.feeSol}) | ` +
        `waiting ${r.waitingCount} | match ${r.matchInProgress ? 'live' : 'idle'}`,
    );
  }
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});
```

The `FASTPOKER_PROGRAM_ID` import is used implicitly inside `getSngPoolPda`. Pass your own keyed RPC via `RPC_URL` for production load.

## Result

You get all 21 pools with their tier economics, current queue depth, and a busy flag, with no API dependency. Pools that return `null` are not yet initialized on the cluster you queried. Once initialized, a pool address never changes, so you can cache the 21 PDAs and re-read only the account data on each refresh.

## Pitfalls

* Do not batch more than 10 accounts in one `getMultipleAccountsInfo` call against public RPC. Some endpoints reject larger batches outright. Chunk to 10 or fewer.
* Read `entry_amount` and `fee_amount` from the account. Do not hard-code lamports; the table here is for display, the bytes are the source of truth.
* `game_type` and `tier` are single bytes in the seed. Do not encode them as `u16` or `u32`, or you will derive the wrong PDA.
* A `null` account is "not initialized," not an error. Render it as an empty pool, not a failure.
* `waiting_count` is the pool-level total across all queue pages. It is not the count of any single page.
* `active_match_set = 1` means a match is preparing or seating. A non-zero `waiting_count` with `active_match_set = 0` is the normal joinable state.
* The discriminant-0 tier enum name is `Micro` in the program, but the public name is Copper. Show Copper to users.

## See also

* [reading-accounts.md](/building-on-the-protocol/reading-accounts.md) - the general account-read and decode pattern.
* [joining-a-sng.md](/building-on-the-protocol/joining-a-sng.md) - sending the join transaction once you have picked a pool.
* [../04-architecture/state-accounts.md](/architecture/state-accounts.md) - the full SngPool, queue page, and match account model.
* [setup.md](/building-on-the-protocol/setup.md) - the shared helper with program IDs and PDA derivations.
