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

# Reading and decoding accounts

> 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

This page is the read path for Fast Poker. It covers how to find an account, fetch its raw bytes, confirm the discriminator, decode it from the IDL layout, and respect the limits of a free public RPC. It is read-only: no transactions, no signing, no custody.

It does not reproduce account field layouts. For the byte sizes, seeds, and field meaning of every account, see [../04-architecture/state-accounts.md](/architecture/state-accounts.md).

## When to use / who signs

Use this when a client needs current protocol state: a table phase, a seat balance, an SNG pool count, or a player profile. Reads are unauthenticated RPC calls, so nothing signs. A wallet is only the identity used to derive player-scoped PDAs, not a signer here. Write flows are covered in [frontend-integration.md](/integrations/frontend-integration.md).

Current SNG integrations also read `SngDuelState` for points/duel/maturity, `SngSettlementRecord` for one game's final payout state, `EmissionCtrl` for the live demand multiplier parameters, and the format/tier `PoolIdle` for the last game timestamp. These accounts may be delegated during live play; route reads to the ER when the L1 owner is the Delegation program.

Private accounts are an exception. `DeckState` and `SeatCards` are gated by a permission program and are not readable through a public RPC during live play. See [Private accounts](#private-accounts) below.

## Inputs and constants

| Input                | Value                                          | Notes                                                             |
| -------------------- | ---------------------------------------------- | ----------------------------------------------------------------- |
| Program ID           | `PokerXYdXL2SKNnfGbv1WE7vJHipTpNsfZbZeVvoJLn`  | Owner of every core gameplay account.                             |
| FastPoker owner (L1) | `PokerXYdXL2SKNnfGbv1WE7vJHipTpNsfZbZeVvoJLn`  | The account owner when state lives on Solana L1.                  |
| Delegated owner (ER) | `DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh` | The account owner when state is delegated to an Ephemeral Rollup. |
| IDL                  | `target/idl/fastpoker.json`                    | Source of discriminators and account layouts.                     |

The shared helper module derives every PDA and exports the program IDs. Import it from [./setup.md](/building-on-the-protocol/setup.md). This page reuses `getTablePda`, `getSeatPda`, and `FASTPOKER_PROGRAM_ID` from that module rather than redefining seeds.

For the full seed list and program IDs, see [setup.md](/building-on-the-protocol/setup.md) and [../04-architecture/state-accounts.md](/architecture/state-accounts.md).

## Steps

### 1. Derive the PDA

Every Fast Poker account address is a program-derived address. The pattern is always the same: a fixed seed string, then zero or more typed seeds, hashed against the program ID. Index seeds (`seat_index`, `tier`, `game_type`) are single bytes. Larger index seeds use little-endian encoding (`page_index` is `u16`, `match_id` is `u64`).

```ts
// tableId is a Uint8Array; seatIndex is a number 0..(maxPlayers - 1)
const [tablePda] = getTablePda(tableId);
const [seatPda] = getSeatPda(tablePda, seatIndex);
```

The helper builds these with `PublicKey.findProgramAddressSync`. You never store account addresses: you re-derive them from the identifiers you already hold.

### 2. Fetch the raw account

Read the bytes with `getAccountInfo`. A missing account returns `null`, which is a normal result for a seat that was never initialized or an account that has been closed.

### 3. Check the discriminator

The first 8 bytes of every Anchor account are a type tag. This IDL stores each tag explicitly in the `accounts[].discriminator` array, so you read the expected bytes from the IDL rather than recompute them. Confirm them before decoding so you never deserialize one account type as another. The struct name is the IDL account name, which is `PascalCase` (for example the seat account is `PlayerSeat`, not `Seat`).

Instructions carry their own discriminators in the IDL `instructions[].discriminator` arrays. You only need account discriminators for reads.

### 4. Decode from the IDL layout

Hand the bytes to the Anchor `BorshAccountsCoder` built from the IDL. The coder strips the 8-byte discriminator, reads the Borsh layout, and returns a typed object. Do not hand-roll offsets: the layout is the IDL's job, and the IDL is the verified source. The field meanings are documented in [../04-architecture/state-accounts.md](/architecture/state-accounts.md).

### 5. Confirm the owner (L1 vs ER)

A Fast Poker account can live in one of two places. While state is on Solana L1, the account `owner` is the FastPoker program. While a table is delegated to an Ephemeral Rollup, the L1 copy is owned by the MagicBlock Delegation program `DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh`, and the live state is on the ER. Check `owner` so you know whether the bytes you read are authoritative L1 state or a delegated stub.

| `owner`                                        | Meaning                                                   |
| ---------------------------------------------- | --------------------------------------------------------- |
| `PokerXYdXL2SKNnfGbv1WE7vJHipTpNsfZbZeVvoJLn`  | Live L1 state. Decode normally.                           |
| `DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh` | Delegated to an ER. Read live state from the ER endpoint. |

## Free public RPC limits

A free public RPC pool enforces limits that shape how you batch reads. Plan for them up front.

| Limit                              | Effect                                                       | What to do                                                   |
| ---------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| `getProgramAccounts` blocked       | Full-program scans are rejected.                             | Derive exact PDAs and read them by address. Never enumerate. |
| `getMultipleAccounts` capped at 10 | Batches over 10 accounts are rejected (WAF, not rate limit). | Chunk every batch read into groups of 10 or fewer.           |

Because you cannot scan, the read model is always derive-then-fetch. You compute the addresses you need (for example, every seat of a known table) and request them in chunks of at most 10. Reading the seats of a 9-max table is two chunks. A 6-max table is one.

## Example

This example imports the shared helper from [./setup.md](/building-on-the-protocol/setup.md), derives a table and its seats, reads them within the 10-account batch limit, verifies discriminators, decodes from the IDL, and reports the L1-vs-ER owner.

```ts
import { Connection, PublicKey } from '@solana/web3.js';
import { BorshAccountsCoder } from '@coral-xyz/anchor';
import {
  FASTPOKER_IDL,
  FASTPOKER_PROGRAM_ID,
  DELEGATION_PROGRAM_ID,
  getTablePda,
  getSeatPda,
} from './setup'; // shared helper from ./setup.md

const coder = new BorshAccountsCoder(FASTPOKER_IDL as any);

// IDL account names are PascalCase. The seat struct is "PlayerSeat".
// In Anchor 0.30+, accountDiscriminator is an instance method that reads
// the 8-byte tag from the IDL accounts[].discriminator array.
const accountDiscriminator = (name: string): Buffer =>
  coder.accountDiscriminator(name);

function ownerLabel(owner: PublicKey): 'L1' | 'ER' | 'other' {
  if (owner.equals(FASTPOKER_PROGRAM_ID)) return 'L1';
  if (owner.equals(DELEGATION_PROGRAM_ID)) return 'ER';
  return 'other';
}

// Read up to 10 accounts per call; chunk anything larger.
async function getMultipleChunked(connection: Connection, keys: PublicKey[]) {
  const out: (Awaited<ReturnType<Connection['getMultipleAccountsInfo']>>[number])[] = [];
  for (let i = 0; i < keys.length; i += 10) {
    const chunk = keys.slice(i, i + 10);
    out.push(...(await connection.getMultipleAccountsInfo(chunk)));
  }
  return out;
}

function decode<T = any>(name: string, data: Buffer): T | null {
  const want = accountDiscriminator(name);
  if (data.length < 8 || !data.subarray(0, 8).equals(want)) return null;
  return coder.decode<T>(name, data);
}

async function readTable(connection: Connection, tableId: Uint8Array) {
  const [tablePda] = getTablePda(tableId);

  const tableInfo = await connection.getAccountInfo(tablePda);
  if (!tableInfo) {
    console.log('table not found:', tablePda.toBase58());
    return;
  }

  const table = decode('Table', tableInfo.data as Buffer);
  if (!table) {
    console.log('not a Table account (discriminator mismatch)');
    return;
  }
  console.log('table:', tablePda.toBase58(), 'owner:', ownerLabel(tableInfo.owner));

  // Derive every seat, then batch-read within the 10-account cap.
  const maxPlayers = Number(table.maxPlayers ?? 9);
  const seatPdas = Array.from({ length: maxPlayers }, (_, i) => getSeatPda(tablePda, i)[0]);
  const seatInfos = await getMultipleChunked(connection, seatPdas);

  seatInfos.forEach((info, i) => {
    if (!info) {
      console.log(`seat ${i}: uninitialized`);
      return; // null is a normal result
    }
    const seat = decode('PlayerSeat', info.data as Buffer);
    if (!seat) {
      console.log(`seat ${i}: discriminator mismatch`);
      return;
    }
    console.log(`seat ${i}: owner=${ownerLabel(info.owner)} status=${seat.status}`);
  });
}

// Usage:
// const connection = new Connection('https://api.mainnet-beta.solana.com', 'confirmed');
// await readTable(connection, tableId);
```

## Result

You get typed, owner-tagged state for a known table and its seats, fetched without any program scan and within the public RPC batch cap. A `null` from `getAccountInfo` or a chunk slot means the account does not exist, which is expected for empty seats and closed accounts. A discriminator mismatch means the address does not hold the type you assumed: stop, do not decode.

## Pitfalls

* Do not call `getProgramAccounts` against a public pool. It is blocked. Derive the exact PDAs you need and read them by address.
* Do not exceed 10 accounts in a `getMultipleAccounts` batch on a free pool. Chunk to 10 or fewer. This is a WAF rule, not a rate limit, so retrying without chunking will keep failing.
* Do not decode before checking the discriminator. The seat struct is `PlayerSeat` in the IDL, not `Seat`. Using the wrong name produces the wrong discriminator and a silent mismatch.
* Do not assume the FastPoker program owns the account. A delegated table is owned on L1 by `DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh`, and its live state is on the ER. Check `owner` first.
* Do not expect to read `DeckState` or `SeatCards` externally during live play. They are permission-gated and a public read returns `null` or stale sentinel bytes. See below.
* Do not hand-roll byte offsets. Decode from the IDL layout. Offsets change with the program; the IDL is the verified source.

### Private accounts

`DeckState` (the shuffled deck and entropy) and `SeatCards` (hole cards) are private by design. They are gated by the MagicBlock permission program, so a public RPC read does not return usable contents during a live hand. Treat a `null` or sentinel value as expected, not as an error. Private card access for the connected wallet goes through scoped TEE authorization, described in [frontend-integration.md](/integrations/frontend-integration.md), not through direct account reads.

## See also

* [setup.md](/building-on-the-protocol/setup.md): the shared helper, program IDs, and PDA-derivation functions.
* [deriving-pools.md](/building-on-the-protocol/deriving-pools.md): deriving SNG pool PDAs.
* [../04-architecture/state-accounts.md](/architecture/state-accounts.md): account sizes, seeds, and field layouts.
* [frontend-integration.md](/integrations/frontend-integration.md): write flows and TEE card access.
