> 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/joining-a-sng.md).

# Joining and leaving a Sit and Go

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

**Wallet signs; the operator network seats you.**

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

## Purpose

A Sit and Go (SNG) starts when enough players queue for the same format and tier. You do not create or join a table directly. You join a shared pool by paying the entry plus fee, which writes a queue marker for your wallet. The operator network then assembles a match, creates a table, and seats you. This page covers the two player-signed instructions: `join_sng_pool` and `leave_sng_pool`.

Before signing, disclose the rule family. Native HU is classic winner-take-all. Every 6-max/9-max entrant starts with one Flat Bounty point; half of SOL pays by final points, half by the ITM curve, and the matured player Raw $FP pool pays by points. These formats also schedule duels from display Level 4. See [Flat Bounty and SnG Duels](/for-players/sng-duels.md).

## When to use / who signs

* Call `join_sng_pool` to enter a pool. Your wallet signs and pays. The instruction creates a queue marker PDA owned by the program and transfers your buy-in into the shared pool vault.
* Call `leave_sng_pool` to withdraw before you are seated. Your wallet signs. The instruction closes your marker and refunds your buy-in from the pool vault.
* You can only leave while your marker is still `Waiting`. Once an operator locks your slot into a match, you are committed and the refund path is closed.
* Your wallet must already be registered (a `Player` PDA must exist). See [../04-architecture/state-accounts.md](/architecture/state-accounts.md).

## Inputs and constants

Pools are keyed by `(game_type, tier)`. The `game_type` byte selects the format. The `tier` byte selects the stake.

| game\_type | Format   |
| ---------- | -------- |
| 0          | Heads-up |
| 1          | 6-max    |
| 2          | 9-max    |

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

The fee is 10% of the total. The prize is 90%. Read the live `entry_amount` and `fee_amount` from the on-chain `SngPool` account rather than hardcoding lamports. See [deriving-pools.md](/building-on-the-protocol/deriving-pools.md) for how to find the pool, its current vault, and its tail queue page.

The `SngPool` account is 150 bytes (including the 8-byte Anchor discriminator). Two fields drive the join and leave flow: `waiting_count` (u32) at offset 27 and `active_match_set` (bool) at offset 83. Decode the account with the IDL instead of reading raw offsets. For the full layout see [../04-architecture/state-accounts.md](/architecture/state-accounts.md).

### join\_sng\_pool

Discriminator: `[213, 144, 203, 165, 20, 91, 35, 180]`.

Arguments:

| Arg               | Type   | Meaning                                                                                                              |
| ----------------- | ------ | -------------------------------------------------------------------------------------------------------------------- |
| `approved_signer` | pubkey | The session key the dealer is allowed to authorize for your seat actions. Must not be the default (all-zero) pubkey. |
| `mini_opt_in`     | bool   | Opt into the Mini jackpot add-on. When true, an extra escrow amount is collected on join and refunded on leave.      |

Accounts, in order:

| # | Account          | Signer | Writable | Notes                                                       |
| - | ---------------- | ------ | -------- | ----------------------------------------------------------- |
| 0 | `player`         | yes    | yes      | Your wallet. Pays the buy-in and the marker rent.           |
| 1 | `player_account` | no     | no       | Your `Player` PDA. Must be registered.                      |
| 2 | `sng_pool`       | no     | yes      | The `SngPool` for this `(game_type, tier)`.                 |
| 3 | `sng_pool_vault` | no     | yes      | Shared pool vault that receives the buy-in.                 |
| 4 | `queue_page`     | no     | yes      | The pool's current tail queue page (append target).         |
| 5 | `queue_marker`   | no     | yes      | Your per-wallet marker PDA. Created by this instruction.    |
| 6 | `protocol_guard` | no     | no       | Pause guard. Join is rejected when the SNG queue is paused. |
| 7 | `clock`          | no     | no       | Clock sysvar.                                               |
| 8 | `system_program` | no     | no       | System program.                                             |

The total debited is the entry plus the fee, plus a small non-refundable queue-page rent contribution (collected only while the pool is funding its next page), plus the marker rent, plus the Mini escrow when you opt in.

### leave\_sng\_pool

Discriminator: `[236, 198, 78, 1, 38, 160, 83, 144]`.

Arguments: none.

Accounts, in order:

| # | Account          | Signer | Writable | Notes                                                       |
| - | ---------------- | ------ | -------- | ----------------------------------------------------------- |
| 0 | `player`         | yes    | yes      | Your wallet. Receives the refund and the marker rent.       |
| 1 | `sng_pool`       | no     | yes      | The `SngPool` you joined.                                   |
| 2 | `sng_pool_vault` | no     | yes      | Pool vault that pays the refund.                            |
| 3 | `queue_marker`   | no     | yes      | Your marker PDA. Closed to your wallet by this instruction. |
| 4 | `queue_page`     | no     | yes      | The page that holds your slot (`queue_marker.page_index`).  |
| 5 | `system_program` | no     | no       | System program.                                             |

The refund is the entry plus the fee, plus the Mini escrow when you opted in. The marker rent is returned separately when the marker account is closed.

## PDA derivation

All PDAs derive against the FastPoker program ID.

| PDA              | Seeds                                              |
| ---------------- | -------------------------------------------------- |
| `SngPool`        | `["sng_pool", game_type: u8, tier: u8]`            |
| `SngPoolVault`   | `["sng_pool_vault", game_type: u8, tier: u8]`      |
| `SngQueuePage`   | `["sng_queue_page", sng_pool, page_index: u16 LE]` |
| `SngQueueMarker` | `["sng_queue_marker", sng_pool, player]`           |
| `Player`         | `["player", wallet]`                               |

The helper in [setup.md](/building-on-the-protocol/setup.md) exposes `getSngPoolPda`, `getSngPoolVaultPda`, `getSngQueuePagePda`, `getSngQueueMarkerPda`, and `getPlayerPda` for these.

## Steps

1. Pick a `game_type` and `tier`. Derive the `SngPool` and `SngPoolVault` PDAs.
2. Read the `SngPool` account. Confirm it is not mid-match (the join is rejected while a match is set against the pool) and read its `tail_page_index`.
3. Derive the tail `SngQueuePage` PDA from `tail_page_index`.
4. Derive your `SngQueueMarker` PDA and your `Player` PDA.
5. Build `join_sng_pool` with `approved_signer` set to your session key and `mini_opt_in` set as desired.
6. Sign and send. Confirm. Your marker is now `Waiting`.
7. To exit before being seated, build `leave_sng_pool` with the same pool, vault, marker, and the page that holds your slot, then sign and send.

## Example

```ts
// join-sng.ts
import {
  Connection,
  Keypair,
  PublicKey,
  SystemProgram,
  SYSVAR_CLOCK_PUBKEY,
  TransactionInstruction,
} from '@solana/web3.js';
import { BorshCoder } from '@coral-xyz/anchor';
import {
  FASTPOKER_IDL,
  FASTPOKER_PROGRAM_ID,
  getSngPoolPda,
  getSngPoolVaultPda,
  getSngQueuePagePda,
  getSngQueueMarkerPda,
  getPlayerPda,
  sendAndConfirm,
} from './setup'; // shared helper from ./setup.md

// Inputs.
const GAME_TYPE = 0; // 0 = heads-up, 1 = 6-max, 2 = 9-max
const TIER = 1;      // 1 = Bronze
const MINI_OPT_IN = false;

export async function joinSng(
  connection: Connection,
  player: Keypair,
  approvedSigner: Keypair,
): Promise<string> {
  const coder = new BorshCoder(FASTPOKER_IDL as any);

  const [sngPool] = getSngPoolPda(GAME_TYPE, TIER);
  const [sngPoolVault] = getSngPoolVaultPda(GAME_TYPE, TIER);
  const [queueMarker] = getSngQueueMarkerPda(sngPool, player.publicKey);
  const [playerAccount] = getPlayerPda(player.publicKey);

  // Read the pool to find the current append page and confirm it is joinable.
  const poolInfo = await connection.getAccountInfo(sngPool);
  if (!poolInfo) throw new Error('SngPool not found for this game_type/tier');
  const pool = coder.accounts.decode('SngPool', poolInfo.data);
  if (pool.activeMatchSet) throw new Error('Pool is mid-match; retry once it seats');
  const [queuePage] = getSngQueuePagePda(sngPool, pool.tailPageIndex);

  // protocol_guard is a singleton PDA seeded by ["protocol_guard"].
  const [protocolGuard] = PublicKey.findProgramAddressSync(
    [Buffer.from('protocol_guard')],
    FASTPOKER_PROGRAM_ID,
  );

  const data = coder.instruction.encode('join_sng_pool', {
    approved_signer: approvedSigner.publicKey,
    mini_opt_in: MINI_OPT_IN,
  });

  const ix = new TransactionInstruction({
    programId: FASTPOKER_PROGRAM_ID,
    keys: [
      { pubkey: player.publicKey, isSigner: true, isWritable: true },
      { pubkey: playerAccount, isSigner: false, isWritable: false },
      { pubkey: sngPool, isSigner: false, isWritable: true },
      { pubkey: sngPoolVault, isSigner: false, isWritable: true },
      { pubkey: queuePage, isSigner: false, isWritable: true },
      { pubkey: queueMarker, isSigner: false, isWritable: true },
      { pubkey: protocolGuard, isSigner: false, isWritable: false },
      { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
      { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
    ],
    data,
  });

  return sendAndConfirm(connection, [ix], [player]);
}
```

## Result

After `join_sng_pool` confirms:

* Your buy-in (entry plus fee, plus any Mini escrow) sits in the shared pool vault.
* A `SngQueueMarker` PDA exists for your wallet with status `Waiting`, recording the page and slot you occupy.
* The pool's `waiting_count` increments. When it reaches `max_players`, a match cooldown timer arms and the pool becomes match-eligible.

From here the operator network drives the rest. A crank operator runs `prepare_sng_match` then `finalize_sng_match` to select and freeze your set of players, then `seat_from_pool` to seat you into a freshly created table, then `start_game` to deal the first hand. These instructions are operator-signed and are not called by players. See [../03-operators/sng-pipeline.md](/for-operators-dealer-service/sng-pipeline.md).

For a current 6-max/9-max table, the operator must also initialize/validate the table's `SngDuelState` and the per-game `SngSettlementRecord`, then delegate them with gameplay state. The matching `EmissionCtrl` and `PoolIdle` are required when the tournament's emission snapshot is funded.

After `leave_sng_pool` confirms, your marker is closed, your slot is freed, and your full buy-in plus marker rent returns to your wallet.

## Pitfalls

* **Default `approved_signer` is rejected.** The instruction requires a non-zero `approved_signer`. Pass a real session key.
* **Joins are blocked mid-match.** While a match is set against the pool (`active_match_set`), new joins are rejected to avoid commingling deposits with an in-flight match's vault draws. Read the pool first and retry once it seats or is cancelled.
* **Use the tail page.** `queue_page` must be the pool's current `tail_page_index`. A stale page index fails the queue-page check. Re-read the pool right before building the join.
* **Leave only works while `Waiting`.** Once an operator locks your slot into a match, the marker is no longer `Waiting` and `leave_sng_pool` reverts. There is no player-side refund after that point; settlement happens through the match path.
* **Pass the page that holds your slot.** `leave_sng_pool` needs the page at `queue_marker.page_index`, not necessarily the tail. Read your marker to find it.
* **One marker per wallet per pool.** The marker PDA is seeded by `(pool, wallet)`. You cannot hold two markers in the same pool at once. Leave first, then rejoin.
* **Read amounts from chain.** The buy-in figures above are mainnet defaults at `TIER_SCALE = 1`. Trust `entry_amount` and `fee_amount` on the live `SngPool` rather than constants in your client.

## See also

* [deriving-pools.md](/building-on-the-protocol/deriving-pools.md)
* [sitting-and-deposits.md](/building-on-the-protocol/sitting-and-deposits.md)
* [../03-operators/sng-pipeline.md](/for-operators-dealer-service/sng-pipeline.md)
* [../04-architecture/state-accounts.md](/architecture/state-accounts.md)
