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

# Private tables

> **Historical reference.** New cash tables and cash gameplay are retired. These instructions describe legacy state and must not be used to start or fund a new table. Current integrations should expose recovery and close only.

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

**Creator wallet signs.** Only the table creator can add or remove whitelist entries on a private table.

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

## Purpose

A private table restricts who can sit. The table is flagged `is_private` at creation and that flag is immutable. Anyone other than the creator must hold a `WhitelistEntry` account for that table before they can deposit, seat, or be dealt in. This page covers the two creator-only instructions that manage the list, `add_whitelist` and `remove_whitelist`, how to read a `WhitelistEntry`, and how the program enforces the list when a hand starts.

The whitelist is a permission gate. It is not the same thing as private hole cards. Card privacy is the MagicBlock permission program. See [../04-architecture/permissions.md](/architecture/permissions.md) for that boundary.

## When to use / who signs

Use this when you operate a private cash table and need to add or remove allowed players. The creator wallet is the only signer that `add_whitelist` and `remove_whitelist` accept. The handler reads the `Table` bytes and rejects any signer that is not `table.creator`, and rejects any table that is not private.

The creator is always implicitly whitelisted. You never create a `WhitelistEntry` for the creator. Every other wallet needs one.

| Action               | Signer  | Effect                                                     |
| -------------------- | ------- | ---------------------------------------------------------- |
| Create private table | Creator | Sets `is_private = true` (immutable).                      |
| `add_whitelist`      | Creator | Creates a `WhitelistEntry` PDA for one player.             |
| `remove_whitelist`   | Creator | Closes that player's `WhitelistEntry`, rent to creator.    |
| Read whitelist       | None    | Derive the PDA and fetch it, or fetch to check membership. |

For the full role-to-signer mapping across every instruction, see [signer-matrix.md](/building-on-the-protocol/signer-matrix.md).

## Inputs and constants

Both instructions take a single argument, the `player` pubkey, and derive the `WhitelistEntry` PDA from it.

### WhitelistEntry PDA

The PDA derives against the FastPoker program ID:

```
seeds = ["whitelist", table, player]
```

`table` is the table PDA. `player` is the 32-byte wallet being whitelisted. The seed string `whitelist` is byte-exact (`[119,104,105,116,101,108,105,115,116]` in the IDL).

### add\_whitelist

| Field         | Value                                  |
| ------------- | -------------------------------------- |
| Arg           | `player: pubkey`                       |
| Discriminator | `[215, 46, 143, 176, 108, 113, 24, 1]` |

Accounts, in order:

| # | Account           | Signer | Writable | Notes                                                                                      |
| - | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------ |
| 0 | `creator`         | yes    | yes      | Pays rent for the new PDA. Must equal `table.creator`.                                     |
| 1 | `table`           | no     | no       | Unchecked. Handler validates discriminator, PDA, creator, and the private flag from bytes. |
| 2 | `whitelist_entry` | no     | yes      | `init`. PDA from `["whitelist", table, player]`.                                           |
| 3 | `system_program`  | no     | no       | `11111111111111111111111111111111`.                                                        |

### remove\_whitelist

| Field         | Value                                   |
| ------------- | --------------------------------------- |
| Arg           | `player: pubkey`                        |
| Discriminator | `[148, 244, 73, 234, 131, 55, 247, 90]` |

Accounts, in order:

| # | Account           | Signer | Writable | Notes                                                       |
| - | ----------------- | ------ | -------- | ----------------------------------------------------------- |
| 0 | `creator`         | yes    | yes      | Receives the closed PDA's rent. Must equal `table.creator`. |
| 1 | `table`           | no     | no       | Unchecked. Same byte-level validation as `add_whitelist`.   |
| 2 | `whitelist_entry` | no     | yes      | `close = creator`. Same PDA seeds.                          |

`remove_whitelist` has no `system_program` account. It closes an existing PDA rather than creating one.

### WhitelistEntry account

The account is 81 bytes including the 8-byte discriminator. The IDL account name is `WhitelistEntry`.

| Field           | Type     | Bytes  | Meaning                                   |
| --------------- | -------- | ------ | ----------------------------------------- |
| (discriminator) | \[u8; 8] | 0..8   | `SHA256("account:WhitelistEntry")[0..8]`. |
| `table`         | pubkey   | 8..40  | Table this entry belongs to.              |
| `player`        | pubkey   | 40..72 | Wallet allowed to join.                   |
| `added_at`      | i64      | 72..80 | Unix timestamp when added.                |
| `bump`          | u8       | 80     | PDA bump.                                 |

For where `WhitelistEntry` sits in the wider account model, see [../04-architecture/state-accounts.md](/architecture/state-accounts.md).

## Steps

1. Confirm the table is private and you sign as the creator. The handler reads `table.is_private` and `table.creator` from bytes and reverts otherwise.
2. Derive the `WhitelistEntry` PDA with `["whitelist", table, player]`.
3. To add a player, build `add_whitelist` with the four accounts above and the `player` arg. The instruction `init`s the PDA and the creator pays rent.
4. To remove a player, build `remove_whitelist` with the three accounts above and the `player` arg. The instruction closes the PDA and returns rent to the creator.
5. To check membership off-chain, fetch the derived PDA. A non-null account owned by the program means the player is whitelisted. A `null` means they are not.
6. Note that on a delegated (live) table the `table` account is owned by the MagicBlock Delegation program on L1. The handler accepts an unchecked `table` and validates it from bytes, so the call works in either state, but the whitelist itself lives on L1.

## How start\_game enforces it

Whitelist removal takes effect at the hand boundary, not instantly. `start_game` is where the program acts on the current list for a private cash table.

When the table is private and `start_game` receives seat and whitelist accounts in `remaining_accounts`, it walks every occupied seat. For each non-creator wallet it re-derives the expected `WhitelistEntry` PDA and checks that the witness account is present, program-owned, and that its `table` and `player` bytes match. A seat whose whitelist witness is missing or invalid is marked revoked.

Revoked seats are not dealt the next hand. The program snapshots the player's chips plus reserve into the normal `Leaving` cashout path, zeroes their chips and reserve, sets the seat to `Leaving`, and clears their bit from the table's occupancy, all-in, folded, and blinds masks. The removed player is then made whole through the standard cashout flow.

The whitelist PDA must still be passed as a witness even if it was closed by `remove_whitelist`. This prevents a crank from choosing who to remove by simply omitting witnesses. A closed PDA reads as not program-owned, which is exactly the revoke signal.

The same private-table check gates the entry points. `seat_player`, `deposit_for_join`, and `join_table` each require a valid `WhitelistEntry` witness for any non-creator wallet before that wallet can take a seat. The creator is exempt at all of these points.

## Example

This example imports the shared helper from [./setup.md](/building-on-the-protocol/setup.md), derives the `WhitelistEntry` PDA, and builds both instructions with the verified discriminators. The creator signs and pays.

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

// Verified instruction discriminators (fastpoker IDL).
const ADD_WHITELIST_DISC = Buffer.from([215, 46, 143, 176, 108, 113, 24, 1]);
const REMOVE_WHITELIST_DISC = Buffer.from([148, 244, 73, 234, 131, 55, 247, 90]);

const WHITELIST_SEED = Buffer.from('whitelist');

// PDA: ["whitelist", table, player].
function getWhitelistEntryPda(table: PublicKey, player: PublicKey) {
  return PublicKey.findProgramAddressSync(
    [WHITELIST_SEED, table.toBuffer(), player.toBuffer()],
    FASTPOKER_PROGRAM_ID,
  );
}

function buildAddWhitelistIx(
  creator: PublicKey,
  table: PublicKey,
  player: PublicKey,
): TransactionInstruction {
  const [whitelistEntry] = getWhitelistEntryPda(table, player);
  return new TransactionInstruction({
    programId: FASTPOKER_PROGRAM_ID,
    keys: [
      { pubkey: creator, isSigner: true, isWritable: true },
      { pubkey: table, isSigner: false, isWritable: false },
      { pubkey: whitelistEntry, isSigner: false, isWritable: true },
      { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
    ],
    data: Buffer.concat([ADD_WHITELIST_DISC, player.toBuffer()]),
  });
}

function buildRemoveWhitelistIx(
  creator: PublicKey,
  table: PublicKey,
  player: PublicKey,
): TransactionInstruction {
  const [whitelistEntry] = getWhitelistEntryPda(table, player);
  return new TransactionInstruction({
    programId: FASTPOKER_PROGRAM_ID,
    keys: [
      { pubkey: creator, isSigner: true, isWritable: true },
      { pubkey: table, isSigner: false, isWritable: false },
      { pubkey: whitelistEntry, isSigner: false, isWritable: true },
    ],
    data: Buffer.concat([REMOVE_WHITELIST_DISC, player.toBuffer()]),
  });
}

// Read membership: fetch the PDA. Non-null + program-owned = whitelisted.
async function isWhitelisted(
  connection: Connection,
  table: PublicKey,
  player: PublicKey,
): Promise<boolean> {
  const [pda] = getWhitelistEntryPda(table, player);
  const info = await connection.getAccountInfo(pda);
  return Boolean(info && info.owner.equals(FASTPOKER_PROGRAM_ID));
}

async function main() {
  const connection = new Connection(
    process.env.RPC_URL ?? 'https://api.mainnet-beta.solana.com',
    'confirmed',
  );

  // Creator keypair. Replace with your own wallet.
  const creator = Keypair.generate();

  // A known private table PDA and a player to allow.
  const table = new PublicKey('11111111111111111111111111111111'); // replace
  const player = new PublicKey('11111111111111111111111111111111'); // replace

  // Add the player.
  const addIx = buildAddWhitelistIx(creator.publicKey, table, player);
  const addSig = await sendAndConfirm(connection, [addIx], [creator]);
  console.log('added', addSig);
  console.log('whitelisted?', await isWhitelisted(connection, table, player));

  // Later, remove the player. Rent returns to the creator.
  const removeIx = buildRemoveWhitelistIx(creator.publicKey, table, player);
  const removeSig = await sendAndConfirm(connection, [removeIx], [creator]);
  console.log('removed', removeSig);
}

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

## Result

After `add_whitelist`, a `WhitelistEntry` PDA exists for `(table, player)` and the player can deposit, seat, and be dealt in. After `remove_whitelist`, that PDA is closed, rent returns to the creator, and the player is revoked at the next `start_game` for that table: their chips and reserve are pushed to the `Leaving` cashout path and their seat is cleared.

Reads are deterministic. The same `(table, player)` always derives the same PDA, so any client can check membership with one `getAccountInfo`.

## Pitfalls

* Only the creator can manage the list. The handler reverts with `Unauthorized` if the signer is not `table.creator`. There is no admin or co-creator role here.
* The table must be private. `add_whitelist` and `remove_whitelist` revert with `InvalidTableConfig` on a public table. The `is_private` flag is set at creation and cannot be changed.
* Do not create a `WhitelistEntry` for the creator. The creator is implicitly allowed at every entry point and at `start_game`.
* Removal is not instant. A removed player keeps their current seat until the next `start_game`, which moves them to `Leaving`. They are not dealt a new hand.
* The whitelist witness is still required after removal. `start_game` expects the closed PDA address to be passed so a crank cannot pick who to revoke by omitting accounts. The closed PDA reads as not program-owned, which is the revoke signal.
* The whitelist lives on L1. A live table is delegated, but the `WhitelistEntry` and the private flag are L1 state. Manage the list against your L1 RPC.
* This is access control, not card privacy. Hole-card privacy is a separate mechanism. See [../04-architecture/permissions.md](/architecture/permissions.md).

## See also

* [creating-a-cash-table.md](/building-on-the-protocol/creating-a-cash-table.md): creating the table and setting the private flag.
* [signer-matrix.md](/building-on-the-protocol/signer-matrix.md): which wallet signs each instruction.
* [../04-architecture/permissions.md](/architecture/permissions.md): the permission boundary and card privacy.
* [../04-architecture/state-accounts.md](/architecture/state-accounts.md): the full account model.
* [setup.md](/building-on-the-protocol/setup.md): the shared helper with program IDs and PDA derivations.
