> 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-player-state.md).

# Reading player, funds and jackpots

> 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 everything tied to a single wallet: profile and XP, claimable funds, the table vault that holds buy-ins, and the jackpot accounts. It is read-only. Nothing here signs or moves SOL. You derive a PDA, fetch its bytes, confirm the discriminator, and decode from the IDL.

It covers three groups of accounts:

* Player and funds: `PlayerAccount` (XP, lifetime stats, `claimable_sol`), `UnclaimedBalance`, the table `TableVault`, `DepositProof`, `PlayerTableMarker`, and the Steel unrefined $FP account.
* Jackpots: `JackpotGlobal`, `JackpotBucket`, `JackpotEntry`, `SngJackpotTableState`, and JPV1 receipts (memo payloads, not accounts).
* Flat Bounty: `SngDuelState` for per-seat point units, active-duel state, and final blind level; `SngSettlementRecord` for one game's final per-seat SOL/$FP values.
* Operator rewards: read-only pointers to `OperatorClaim` and `OperatorRewardTotal`.

This page does not reproduce account field layouts or economics. For sizes, seeds, and field meaning see [../04-architecture/state-accounts.md](/architecture/state-accounts.md). For the player-facing meaning of XP and jackpots see [../02-players/xp-rewards.md](/for-players/xp-rewards.md) and [../02-players/jackpots.md](/for-players/jackpots.md).

## When to use / who signs

Use this when a client needs a player's profile, balance, or jackpot status: a stats panel, a claimable-funds badge, a jackpot ticker, or an operator earnings view. Reads are unauthenticated RPC calls, so nothing signs. A wallet pubkey is only the identity used to derive player-scoped PDAs, not a signer here. The claim and cashout write flows are covered in [leaving-and-recovery.md](/building-on-the-protocol/leaving-and-recovery.md).

## Inputs and constants

| Input            | Value                                         | Notes                                 |
| ---------------- | --------------------------------------------- | ------------------------------------- |
| Program ID       | `PokerXYdXL2SKNnfGbv1WE7vJHipTpNsfZbZeVvoJLn` | Owner of every account on this page.  |
| IDL              | `target/idl/fastpoker.json`                   | Source of discriminators and layouts. |
| SPL Memo program | `MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr` | Carries JPV1 receipts.                |

The shared helper from [./setup.md](/building-on-the-protocol/setup.md) exports the program IDs and derives `getPlayerPda`, `getTableVaultPda`, and the SNG helpers. This page reuses those and adds the few seeds setup does not export. The IDL account names are `PascalCase` (for example the seat account is `PlayerSeat`, not `Seat`).

### Seeds used on this page

All PDAs derive against the FastPoker program ID. `table` is a `Table` PDA pubkey. `wallet`, `player`, and `operator` are wallet pubkeys. `seat_index`, `game_type`, and `tier` are single bytes.

| Account                    | Seeds                                           | In setup helper    |
| -------------------------- | ----------------------------------------------- | ------------------ |
| `PlayerAccount`            | `["player", wallet]`                            | `getPlayerPda`     |
| `UnclaimedBalance`         | `["unclaimed", table, player]`                  | no                 |
| `TableVault`               | `["vault", table]`                              | `getTableVaultPda` |
| `DepositProof`             | derived at deposit time, read by address        | no                 |
| `PlayerTableMarker`        | `["player_table", player, table]`               | no                 |
| `JackpotGlobal`            | `["jackpot_global"]`                            | no                 |
| `JackpotBucket`            | `["jackpot_bucket", game_type, tier]`           | no                 |
| `JackpotEntry`             | `["jackpot_entry", table, seat_index]`          | no                 |
| `SngJackpotTableState`     | `["sng_jackpot_table", table]`                  | no                 |
| `SngDuelState`             | `["sng_duel", table]`                           | no                 |
| `SngSettlementRecord`      | `["sng_settlement", table, start_hand: u64 LE]` | no                 |
| `EmissionCtrl`             | `["emission_ctrl"]`                             | no                 |
| `PoolIdle`                 | `["pool_idle", game_type, tier]`                | no                 |
| `OperatorClaim`            | `["op_claim", table, operator]`                 | no                 |
| `OperatorRewardTotal` (ER) | `["op_reward_total_er", table]`                 | no                 |
| `OperatorRewardTotal` (L1) | `["op_reward_total_l1", table]`                 | no                 |

`JackpotEntry` is per seat, not per wallet. To map an entry to a wallet, read `JackpotEntry.wallet`, or read the corresponding `PlayerSeat` at the same `seat_index` and compare `wallet`.

### Account discriminators

| Account                | Discriminator (first 8 bytes)             | Decode path |
| ---------------------- | ----------------------------------------- | ----------- |
| `PlayerAccount`        | `[224, 184, 224, 50, 98, 72, 48, 236]`    | IDL         |
| `UnclaimedBalance`     | `[205, 109, 0, 175, 61, 192, 87, 247]`    | IDL         |
| `TableVault`           | `[7, 14, 145, 251, 151, 162, 15, 125]`    | IDL         |
| `DepositProof`         | `[53, 219, 92, 250, 122, 27, 232, 246]`   | IDL         |
| `PlayerTableMarker`    | `[124, 89, 140, 43, 170, 207, 251, 230]`  | IDL         |
| `JackpotGlobal`        | `[52, 130, 68, 82, 159, 209, 164, 156]`   | IDL         |
| `JackpotEntry`         | `[115, 191, 163, 74, 14, 14, 99, 56]`     | IDL         |
| `SngJackpotTableState` | `[175, 110, 165, 90, 21, 170, 180, 213]`  | IDL         |
| `SngDuelState`         | `[187, 105, 215, 92, 106, 127, 211, 184]` | IDL         |
| `SngSettlementRecord`  | `[106, 241, 75, 227, 64, 147, 124, 173]`  | IDL         |
| `EmissionCtrl`         | `[41, 171, 243, 212, 6, 6, 72, 94]`       | IDL         |
| `PoolIdle`             | `[107, 28, 178, 1, 234, 172, 111, 208]`   | IDL         |
| `OperatorClaim`        | `[61, 131, 237, 220, 17, 24, 245, 161]`   | IDL         |
| `OperatorRewardTotal`  | `[177, 97, 180, 184, 150, 182, 54, 236]`  | IDL         |
| `JackpotBucket`        | `[130, 178, 152, 235, 52, 67, 10, 188]`   | raw bytes   |

`JackpotBucket` is a manually serialized account, not an Anchor struct in the IDL. The Anchor `BorshAccountsCoder` cannot decode it. Confirm the discriminator above, read `game_type` at offset 8 and `tier` at offset 9, and treat the rest as opaque per-table bitmap state. The lobby does not need its body.

### Fields integrations read most

These names are the IDL field names. See [../04-architecture/state-accounts.md](/architecture/state-accounts.md) for the full layout.

| Account                | Field                                                          | Meaning                                                                   |
| ---------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `PlayerAccount`        | `xp` (u64)                                                     | Cumulative XP.                                                            |
| `PlayerAccount`        | `claimable_sol` (u64)                                          | Lamports the wallet can claim.                                            |
| `PlayerAccount`        | `hands_played`, `hands_won`, `tournaments_won`                 | Lifetime counters.                                                        |
| `UnclaimedBalance`     | `amount` (u64)                                                 | Pending lamports awaiting claim for one table.                            |
| `TableVault`           | `total_deposited`, `total_withdrawn` (u64)                     | Vault flow.                                                               |
| `TableVault`           | `token_mint` (pubkey)                                          | Vault asset. Default pubkey means native SOL.                             |
| `DepositProof`         | `buy_in`, `consumed`                                           | Pending seat funding and whether it was used.                             |
| `JackpotGlobal`        | `mini_pool_lamports`, `grand_unrefined_pool` (u64)             | Live pool sizes.                                                          |
| `JackpotEntry`         | `active`, `mini_opt_in`, `grand_weight`                        | Per-seat jackpot status.                                                  |
| `SngJackpotTableState` | `mini_hits`, `grand_hits`, `last_processed_hand`               | Per-table jackpot history.                                                |
| `SngDuelState`         | `knockout_credit_units`, `fp_bounty_weight_units`              | Fractional Flat Bounty points; divide by 1,000,000 for display.           |
| `SngDuelState`         | `duel_active`, duel seats/choices/round, `final_blind_level`   | Scheduled-duel UI and final maturity state.                               |
| `SngSettlementRecord`  | `finalized`, `paid`, `wallets`, `sol_lamports`, `fp_unrefined` | Per-game payout delta; do not confuse it with cumulative wallet balances. |
| `EmissionCtrl`         | demand signals, target, `min_raw_mult_bps`, idle parameters    | Live governed emission configuration.                                     |
| `PoolIdle`             | `game_type`, `tier`, `last_game_ts`                            | Current cell identity and idle duration input.                            |

`grand_unrefined_pool` is an unrefined accumulator. Display it as a relative size or refine it through the documented conversion, not as a raw SOL number. See [../02-players/jackpots.md](/for-players/jackpots.md).

### JPV1 receipts

A JPV1 receipt is not an account. When a Mini or Grand jackpot resolves on an SNG hand, `settle_sng_hand_jackpot` emits an SPL Memo CPI. You read it from a confirmed transaction, not from `getAccountInfo`. The on-chain memo string is the ASCII prefix `JPV1B64:` followed by the base64 of a raw 131-byte payload. Strip the prefix and base64-decode to get the raw bytes. The raw payload begins with the magic `JPV1` and version byte `1`, then the table pubkey, hand number, seat masks, hit flags, payout lamports, and the rolling-hash chain head. Receipts surface in inner instructions because the Memo CPI fires inside the settle instruction. For verifying a receipt against the hand record it is anchored to, see [hand-history-verification.md](/building-on-the-protocol/hand-history-verification.md).

## Steps

1. Derive the PDA from the seeds above. Player-scoped accounts come from the wallet; table-scoped accounts come from a `Table` PDA.
2. Fetch with `getAccountInfo`. A `null` result is normal: an unregistered wallet has no `PlayerAccount`, a player with no pending funds has no `UnclaimedBalance`, and a seat that never opted into a jackpot has no `JackpotEntry`.
3. Confirm the 8-byte discriminator before decoding. Use the IDL value, never decode blind.
4. Decode. For IDL accounts hand the bytes to `BorshAccountsCoder`. For `JackpotBucket` read the two header bytes directly.
5. For JPV1 receipts, fetch the transaction with `getParsedTransaction` and scan top-level and inner instructions for Memo payloads.
6. Respect the free-pool limits. Do not call `getProgramAccounts`. Chunk `getMultipleAccounts` to 10 or fewer. 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 the per-wallet seeds it does not export, and reads a player profile, an unclaimed balance, a table vault, and the global jackpot. It checks discriminators and handles `null`.

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

const coder = new BorshAccountsCoder(FASTPOKER_IDL as any);

// Seeds not exported by setup.ts (verified against the program source).
const UNCLAIMED_SEED = Buffer.from('unclaimed');
const JACKPOT_GLOBAL_SEED = Buffer.from('jackpot_global');
const JACKPOT_ENTRY_SEED = Buffer.from('jackpot_entry');

const getUnclaimedPda = (table: PublicKey, player: PublicKey) =>
  PublicKey.findProgramAddressSync(
    [UNCLAIMED_SEED, table.toBuffer(), player.toBuffer()], FASTPOKER_PROGRAM_ID);

const getJackpotGlobalPda = () =>
  PublicKey.findProgramAddressSync([JACKPOT_GLOBAL_SEED], FASTPOKER_PROGRAM_ID);

const getJackpotEntryPda = (table: PublicKey, seatIndex: number) =>
  PublicKey.findProgramAddressSync(
    [JACKPOT_ENTRY_SEED, table.toBuffer(), Buffer.from([seatIndex])], FASTPOKER_PROGRAM_ID);

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

const lamportsToSol = (n: bigint | number) => Number(n) / 1e9;

async function readPlayerState(
  connection: Connection,
  wallet: PublicKey,
  table: PublicKey,
) {
  const [playerPda] = getPlayerPda(wallet);
  const [unclaimedPda] = getUnclaimedPda(table, wallet);
  const [vaultPda] = getTableVaultPda(table);
  const [globalPda] = getJackpotGlobalPda();

  // One batched read, well under the 10-account public-pool cap.
  const [playerInfo, unclaimedInfo, vaultInfo, globalInfo] =
    await connection.getMultipleAccountsInfo(
      [playerPda, unclaimedPda, vaultPda, globalPda], 'confirmed');

  // PlayerAccount: profile, XP, claimable SOL. null = unregistered wallet.
  if (playerInfo) {
    const p = decode('PlayerAccount', playerInfo.data as Buffer);
    if (p) {
      console.log('xp', String(p.xp));
      console.log('claimable SOL', lamportsToSol(p.claimableSol));
      console.log('hands played', String(p.handsPlayed), 'won', String(p.handsWon));
    }
  } else {
    console.log('player not registered:', playerPda.toBase58());
  }

  // UnclaimedBalance: pending funds for this table. null = nothing pending.
  if (unclaimedInfo) {
    const u = decode('UnclaimedBalance', unclaimedInfo.data as Buffer);
    if (u) console.log('unclaimed SOL (this table)', lamportsToSol(u.amount));
  } else {
    console.log('no unclaimed balance for this table');
  }

  // TableVault: holds buy-ins. token_mint == default pubkey means native SOL.
  if (vaultInfo) {
    const v = decode('TableVault', vaultInfo.data as Buffer);
    if (v) {
      const isSol = new PublicKey(v.tokenMint).equals(PublicKey.default);
      console.log('vault asset', isSol ? 'SOL' : new PublicKey(v.tokenMint).toBase58());
      console.log('vault deposited', String(v.totalDeposited),
        'withdrawn', String(v.totalWithdrawn));
    }
  }

  // JackpotGlobal: live pool sizes. Mini is in lamports; Grand is unrefined.
  if (globalInfo) {
    const g = decode('JackpotGlobal', globalInfo.data as Buffer);
    if (g) {
      console.log('mini pool SOL', lamportsToSol(g.miniPoolLamports));
      console.log('grand unrefined (raw)', String(g.grandUnrefinedPool));
    }
  }

  // JackpotEntry is per seat. Derive by (table, seatIndex) and read by address.
  const [entryPda] = getJackpotEntryPda(table, 0);
  const entryInfo = await connection.getAccountInfo(entryPda);
  if (entryInfo) {
    const e = decode('JackpotEntry', entryInfo.data as Buffer);
    if (e) {
      console.log('seat 0 jackpot', { active: e.active, miniOptIn: e.miniOptIn,
        wallet: new PublicKey(e.wallet).toBase58() });
    }
  } else {
    console.log('seat 0 has no jackpot entry');
  }
}

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

## Result

You get typed, current state for one wallet: its profile and XP, its claimable SOL, the vault that holds a table's funds, and the global jackpot pools, plus the per-seat jackpot entry. Every `null` is a meaningful, expected state, not an error: no `PlayerAccount` means an unregistered wallet, no `UnclaimedBalance` means nothing pending, no `JackpotEntry` means the seat did not opt into a jackpot. The reads are deterministic, so the same inputs always derive the same addresses and the same values resolve again on each refresh.

For 6-max/9-max, compute maturity as `min((final_blind_level + 1) * 2500, 10000)` bps and apply it to the whole post-Royal player Raw $FP pool before point weights. For jackpot display, read the governed base denominators from `JackpotGlobal`: Lucky uses the base directly; Royal scales it by the table's format/tier contribution. Do not label 2,500 as a universal Royal chance.

## Pitfalls

* Do not treat `JackpotBucket` as an Anchor account. It is manually serialized and is not in the IDL types. Confirm its discriminator and read `game_type` (offset 8) and `tier` (offset 9) directly. Do not pass it to `BorshAccountsCoder`.
* Do not look for a JPV1 account. JPV1 receipts are SPL Memo payloads on a transaction, read with `getParsedTransaction`, scanning inner instructions. They are not in account storage.
* Do not display `grand_unrefined_pool` as SOL. It is an unrefined accumulator, not lamports. Show Mini in SOL from `mini_pool_lamports`, and refine or relativize Grand. See [../02-players/jackpots.md](/for-players/jackpots.md).
* Do not assume `JackpotEntry` is keyed by wallet. It is seeded by `(table, seat_index)`. Read `wallet` from the entry, or match it against the `PlayerSeat` at the same index.
* Do not assume a vault is SOL. Check `TableVault.token_mint`. The default pubkey means native SOL; any other value is an SPL mint.
* Do not call `getProgramAccounts` to enumerate per-wallet accounts on a public pool. It is blocked. Derive exact PDAs and read them by address. Chunk batches to 10 or fewer.
* Do not decode before checking the discriminator. A different account at the derived address (or a length mismatch) means the type assumption is wrong: stop, do not decode.
* `claimable_sol` and `UnclaimedBalance.amount` are read-only here. Moving those funds is a signed write flow, covered in [leaving-and-recovery.md](/building-on-the-protocol/leaving-and-recovery.md).
* Do not use a settlement record's SOL/$FP arrays as the wallet's total claimable balance. They are one game's delta; read Player and Steel state for cumulative claim UI.

## See also

* [reading-accounts.md](/building-on-the-protocol/reading-accounts.md): the general derive, fetch, discriminator, decode pattern and the free-pool limits.
* [hand-history-verification.md](/building-on-the-protocol/hand-history-verification.md): verifying a JPV1 receipt against its hand record.
* [deriving-pools.md](/building-on-the-protocol/deriving-pools.md): deriving the 21 SNG pool PDAs.
* [setup.md](/building-on-the-protocol/setup.md): the shared helper with program IDs and PDA derivations.
* [../04-architecture/state-accounts.md](/architecture/state-accounts.md): account sizes, seeds, and field layouts.
* [../02-players/xp-rewards.md](/for-players/xp-rewards.md): what XP means to a player.
* [../02-players/jackpots.md](/for-players/jackpots.md): Mini and Grand jackpot mechanics.
