> 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/quickstart.md).

# Quickstart

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

**Wallet + RPC only**

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

The current public IDL has 168 instructions, 36 account types, and 199 custom errors. If your local/generated client predates 2026-07-17, refresh it before building SNG flows; record-enabled and duel-mode tables require the new account families and fail-closed errors.

## Purpose

This page is a single end-to-end path. You connect a wallet, read all 21 SNG pools, read one table and its seats, send one `join_sng_pool` instruction, then read back your own queue marker and the pool waiting count. It uses an RPC endpoint and a wallet keypair. It does not use a session key, a delegated signer, or a live ephemeral-rollup table.

Joining a pool queues you. It does not seat you. The crank selects and seats queued players later, on its own schedule. See the Result and Pitfalls sections below.

## When to use / who signs

Use this when you want the smallest working integration against the live program, or as the skeleton for a bot or backend that enters players into Sit and Go queues.

The wallet (the `player`) signs `join_sng_pool`. There is exactly one signer and one instruction. No crank, TEE, or operator signature is involved at queue time. The wallet must already be registered (have a `Player` PDA) before it can join. Registration is covered in [setup.md](/building-on-the-protocol/setup.md).

## Inputs and constants

This page imports the shared helper defined in [setup.md](/building-on-the-protocol/setup.md): program IDs, PDA derivations, and `sendAndConfirm`. It does not redefine them.

Program ID (verified against the IDL `address`):

| Program   | Pubkey                                        |
| --------- | --------------------------------------------- |
| FastPoker | `PokerXYdXL2SKNnfGbv1WE7vJHipTpNsfZbZeVvoJLn` |

SNG pools are keyed by `(gameType, tier)`. There are 3 game types and 7 tiers, so 21 pools.

`GameType` (enum order is the discriminant; mirrored at `Table` field `game_type`):

| Value | Variant           | Players |
| ----- | ----------------- | ------- |
| 0     | `SitAndGoHeadsUp` | 2       |
| 1     | `SitAndGo6Max`    | 6       |
| 2     | `SitAndGo9Max`    | 9       |
| 3     | `CashGame`        | varies  |

Only game types 0, 1, and 2 are SNG pools. Game type 3 (`CashGame`) has no pool.

SNG tiers (mainnet, fee is 10 percent of total, prize is 90 percent):

| Tier byte | Enum     | Name     | Total (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 name is Copper.

`join_sng_pool` takes two arguments:

| Arg               | Type     | Meaning                                                                                                                                   |
| ----------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `approved_signer` | `Pubkey` | The signer the crank will accept for your in-game actions once seated. May be your own wallet. Must not be the default (all-zero) pubkey. |
| `mini_opt_in`     | `bool`   | Opt in to the Mini jackpot add-on. `false` for the minimal path.                                                                          |

Account order for `join_sng_pool` (from the IDL):

| # | Account          | Notes                                            |
| - | ---------------- | ------------------------------------------------ |
| 0 | `player`         | mut, signer (your wallet)                        |
| 1 | `player_account` | PDA `["player", wallet]`                         |
| 2 | `sng_pool`       | mut, PDA `["sng_pool", gameType, tier]`          |
| 3 | `sng_pool_vault` | mut, PDA `["sng_pool_vault", gameType, tier]`    |
| 4 | `queue_page`     | mut, the current tail page                       |
| 5 | `queue_marker`   | mut, PDA `["sng_queue_marker", sngPool, wallet]` |
| 6 | `protocol_guard` | PDA `["protocol_guard"]`                         |
| 7 | `clock`          | `SYSVAR_CLOCK_PUBKEY`                            |
| 8 | `system_program` |                                                  |

The `queue_page` is the pool's current append page. Its index is the pool's `tail_page_index`. Derive it with `["sng_queue_page", sngPool, tail_page_index]`. The full account layouts for `SngPool`, `SngQueuePage`, and `SngQueueMarker` are documented in [../04-architecture/state-accounts.md](/architecture/state-accounts.md). This page does not duplicate those offsets. Pool, vault, queue page, and table-id derivation are explained in [deriving-pools.md](/building-on-the-protocol/deriving-pools.md).

## Steps

1. Connect a wallet and an RPC connection. The wallet must already have a `Player` PDA (see [setup.md](/building-on-the-protocol/setup.md)).
2. Read all 21 SNG pools with one batched `getMultipleAccountsInfo` over the derived `(gameType, tier)` PDAs. Skip any that are missing (an uninitialized pool returns null).
3. Pick a pool. Read its `tail_page_index` field and derive the current `queue_page` PDA.
4. Optionally read a table and its seats to inspect live state. A table is found from its `table_id` (`["table", table_id]`), and each seat is `["seat", table, seat_index]`.
5. Build the `join_sng_pool` instruction in the exact account order above, with `approved_signer` set to a non-default pubkey and `mini_opt_in = false`.
6. Send it. One wallet signature.
7. Read back your `SngQueueMarker` PDA and the pool `waiting_count` to confirm you are queued.

## Example

This example imports the shared helper from [setup.md](/building-on-the-protocol/setup.md) (`./setup`). It uses `@solana/web3.js` for transport and `@coral-xyz/anchor` only to decode account data from the IDL. Replace the keypair and RPC URL with your own.

```ts
// quickstart.ts
import {
  Connection,
  Keypair,
  PublicKey,
  SystemProgram,
  SYSVAR_CLOCK_PUBKEY,
  TransactionInstruction,
} from '@solana/web3.js';
import { BorshAccountsCoder, BorshInstructionCoder, Idl } from '@coral-xyz/anchor';
import {
  FASTPOKER_IDL,
  FASTPOKER_PROGRAM_ID,
  getPlayerPda,
  getSeatPda,
  getSngPoolPda,
  getSngPoolVaultPda,
  getSngQueueMarkerPda,
  getSngQueuePagePda,
  sendAndConfirm,
} from './setup';

const PROTOCOL_GUARD = PublicKey.findProgramAddressSync(
  [Buffer.from('protocol_guard')],
  FASTPOKER_PROGRAM_ID,
)[0];

// GameType x tier: 3 SNG game types (0,1,2) over 7 tiers (0..6) = 21 pools.
const SNG_GAME_TYPES = [0, 1, 2];
const TIERS = [0, 1, 2, 3, 4, 5, 6];
const TIER_NAMES = ['Copper', 'Bronze', 'Silver', 'Gold', 'Platinum', 'Diamond', 'Black'];

async function main() {
  // 1) Wallet + RPC only.
  const connection = new Connection(process.env.RPC_URL ?? 'https://api.mainnet-beta.solana.com', 'confirmed');
  const wallet = Keypair.generate(); // replace with your funded, registered wallet

  const idl = FASTPOKER_IDL as Idl;
  const accCoder = new BorshAccountsCoder(idl);
  const ixCoder = new BorshInstructionCoder(idl);

  // 2) Read all 21 SNG pools in one batched call.
  const poolKeys = SNG_GAME_TYPES.flatMap((gt) =>
    TIERS.map((tier) => ({ gt, tier, pda: getSngPoolPda(gt, tier)[0] })),
  );
  const poolInfos = await connection.getMultipleAccountsInfo(poolKeys.map((p) => p.pda));
  const pools = poolKeys.map((p, i) => {
    const info = poolInfos[i];
    if (!info) return { ...p, exists: false as const };
    const data = accCoder.decode('SngPool', info.data) as {
      tailPageIndex: number;
      waitingCount: number;
      entryAmount: bigint;
      feeAmount: bigint;
    };
    return { ...p, exists: true as const, data };
  });

  for (const p of pools) {
    const label = `${['HU', '6max', '9max'][p.gt]} ${TIER_NAMES[p.tier]}`;
    if (!p.exists) { console.log(`${label}: not initialized`); continue; }
    console.log(`${label}: waiting=${p.data.waitingCount} tail_page=${p.data.tailPageIndex}`);
  }

  // 3) Pick a pool. Copper heads-up (gameType 0, tier 0) for the smallest buy-in.
  const gameType = 0;
  const tier = 0;
  const chosen = pools.find((p) => p.gt === gameType && p.tier === tier);
  if (!chosen?.exists) throw new Error('Chosen pool is not initialized.');

  const [sngPool] = getSngPoolPda(gameType, tier);
  const [sngPoolVault] = getSngPoolVaultPda(gameType, tier);
  const [queuePage] = getSngQueuePagePda(sngPool, chosen.data.tailPageIndex);
  const [playerPda] = getPlayerPda(wallet.publicKey);
  const [queueMarker] = getSngQueueMarkerPda(sngPool, wallet.publicKey);

  // 4) Optional: read a table and seat 0 to inspect live state.
  //    Supply a known table pubkey (TABLE_PUBKEY) to enable this block.
  if (process.env.TABLE_PUBKEY) {
    const table = new PublicKey(process.env.TABLE_PUBKEY);
    const tableInfo = await connection.getAccountInfo(table);
    if (tableInfo) {
      const t = accCoder.decode('Table', tableInfo.data) as {
        maxPlayers: number;
        currentPlayers: number;
      };
      console.log(`table players: ${t.currentPlayers}/${t.maxPlayers}`);
      const seatKeys = Array.from({ length: t.maxPlayers }, (_, i) => getSeatPda(table, i)[0]);
      const seatInfos = await connection.getMultipleAccountsInfo(seatKeys);
      seatInfos.forEach((info, i) => {
        if (!info) { console.log(`seat ${i}: empty`); return; }
        const s = accCoder.decode('PlayerSeat', info.data) as { wallet: PublicKey; chips: bigint };
        console.log(`seat ${i}: ${s.wallet.toBase58()} chips=${s.chips}`);
      });
    }
  }

  // 5) Build join_sng_pool. One player-signed instruction.
  const approvedSigner = wallet.publicKey; // must be non-default
  const data = ixCoder.encode('join_sng_pool', {
    approved_signer: approvedSigner,
    mini_opt_in: false,
  });
  const joinIx = new TransactionInstruction({
    programId: FASTPOKER_PROGRAM_ID,
    keys: [
      { pubkey: wallet.publicKey, isSigner: true, isWritable: true },   // player
      { pubkey: playerPda, isSigner: false, isWritable: false },        // player_account
      { pubkey: sngPool, isSigner: false, isWritable: true },           // sng_pool
      { pubkey: sngPoolVault, isSigner: false, isWritable: true },      // sng_pool_vault
      { pubkey: queuePage, isSigner: false, isWritable: true },         // queue_page
      { pubkey: queueMarker, isSigner: false, isWritable: true },       // queue_marker
      { pubkey: PROTOCOL_GUARD, isSigner: false, isWritable: false },   // protocol_guard
      { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false }, // clock
      { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, // system_program
    ],
    data,
  });

  // 6) Send. One wallet signature.
  const sig = await sendAndConfirm(connection, [joinIx], [wallet]);
  console.log('join_sng_pool:', sig);

  // 7) Read back your queue marker and the pool waiting count.
  const [markerInfo, poolInfo] = await connection.getMultipleAccountsInfo([queueMarker, sngPool]);
  if (markerInfo) {
    const m = accCoder.decode('SngQueueMarker', markerInfo.data) as {
      pageIndex: number;
      slotIndex: number;
      ticket: bigint;
      status: number;
    };
    console.log(`queued: page=${m.pageIndex} slot=${m.slotIndex} ticket=${m.ticket} status=${m.status}`);
  }
  if (poolInfo) {
    const p = accCoder.decode('SngPool', poolInfo.data) as { waitingCount: number };
    console.log(`pool waiting_count: ${p.waitingCount}`);
  }
}

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

## Result

After the transaction confirms:

* A `SngQueueMarker` PDA exists at `["sng_queue_marker", sngPool, wallet]` with `status = 0` (waiting). Its `ticket` is your FIFO position.
* The pool's `waiting_count` increased by 1.
* Your buy-in plus fee moved into the pool vault. For Copper heads-up that is the full 0.05 SOL total. A small queue-page rent contribution and the queue-marker rent are also debited from your wallet.

You are queued, not seated. No table, seat, or cards exist for you yet. The crank watches pools, and once a pool has enough waiting players and its cooldown has passed, the crank prepares a match, selects players, and seats them onto a table. Your `approved_signer` is the key the crank will accept for your actions once that happens. The mechanics of selection and seating are covered in [joining-a-sng.md](/building-on-the-protocol/joining-a-sng.md).

## Pitfalls

* Not registered. `join_sng_pool` requires a `Player` PDA with `is_registered = true`. Register first (see [setup.md](/building-on-the-protocol/setup.md)) or the instruction fails with `PlayerNotRegistered`.
* Default `approved_signer`. The program rejects an all-zero `approved_signer` with `InvalidAccountData`. Pass your wallet or a real delegate.
* Stale tail page. The `queue_page` account must be the pool's current `tail_page_index`. Read the pool immediately before building the instruction. If a join landed between your read and your send, the page can advance and the instruction fails with `InvalidQueuePage`. Re-read and retry.
* Match in progress. If the pool already has a match set (`active_match_set` is true), new joins are rejected with `PoolMatchInProgress`. Retry after the match seats or is cancelled.
* Double join. Each wallet has exactly one marker per pool. A second join while the marker exists fails because the marker PDA is already initialized.
* Queued is not seated. Do not treat a confirmed `join_sng_pool` as being in a hand. Poll the marker `status` and the pool, or watch for your table assignment, before assuming you are playing.
* Uninitialized pools. Not every `(gameType, tier)` pool is initialized. A missing account reads as null. Skip it rather than treating null as an error.

## See also

* [setup.md](/building-on-the-protocol/setup.md): wallet, RPC, registration, and the shared helper this page imports.
* [joining-a-sng.md](/building-on-the-protocol/joining-a-sng.md): how the crank selects and seats queued players.
* [deriving-pools.md](/building-on-the-protocol/deriving-pools.md): pool, vault, queue-page, and SNG table-id derivation.
* [../04-architecture/state-accounts.md](/architecture/state-accounts.md): full `SngPool`, `SngQueuePage`, `SngQueueMarker`, `Table`, and `PlayerSeat` layouts.
