---
name: megapot-read-state
description: Read Megapot protocol state using a two-round read pattern — current drawing, prize tiers, user tickets, LP position, referral balance, subscription state, and config constants.
---

# Read Megapot Protocol State (two-round read pattern)

## What This Does

Uses a two-round read pattern to batch all commonly-needed reads against the Megapot protocol: Round 1 fetches `currentDrawingId` then `getDrawingState` to get the prize pool and ball bounds needed by the payout calculator. Round 2 batches all remaining reads — including `getExpectedDrawingTierPayouts` with the real values from Round 1 — in a single `multicall`. No wallet needed — read-only.

## When to Stay on RPC vs. Hop to the Data API

Mirror of the table in `megapot-data-api`, framed from the RPC side. Use this to decide which side of the divide each read falls on **before** writing any of the recipes below.

| You need... | Use |
|---|---|
| Live current-drawing state (jackpot, time, ball bounds) | **RPC** — this skill (`getDrawingState` / two-round read) |
| Per-tier expected payouts for the active drawing | **RPC** — this skill (`getExpectedDrawingTierPayouts`) |
| A wallet's ticket list — **any** drawing, current or past | **API** — `megapot-data-api` (`GET /v1/wallets/{addr}/tickets`, `…/tickets/rounds/{roundId}`). For current-drawing tickets `matched_normals` / `bonusball_match` / `winnings_amount` are `null` by design — no balls picked yet — and remain valid OpenAPI per the schema. |
| Per-ticket match status / payout for **settled** drawings | **API** — same `…/tickets[/rounds/{roundId}]` endpoint (server-precomputed); narrowed to winning rows on `…/wins`. |
| Real-time post-buy confirmation a freshly-purchased ticket landed (sub-block, no indexer lag) | **RPC** — this skill (`getUserTickets`); pair with `TicketPurchased` event subscription for the streaming side. |
| LP position, referral balance, subscription state | **RPC** — this skill |
| Settlement transitions, ticket-purchase events | **RPC** — event subscriptions (this skill, "Subscribing to Lifecycle Events") |
| Cross-drawing wallet wins / claim status | **API** — `megapot-data-api` (`GET /v1/wallets/{addr}/wins`; add `?claimed=false` for the unclaimed-only feed used by claim UIs) |
| Round-by-round history page (>1 drawing) | **API** — `megapot-data-api` (`GET /v1/rounds?cursor=…`) |
| Per-round leaderboards, wallet lifetime stats | **API** — `megapot-data-api` (`GET /v1/rounds/{id}/wins`, `GET /v1/wallets/{addr}/stats`) |
| Cumulative ticket volume, cumulative prizes paid | **API** — `megapot-data-api` (off-chain aggregate; RPC scans not viable) |

If a row says **API**, the inline recipes below still work but won't scale — see the "Off-chain alternative" callouts that lead each affected section for the matching endpoint.

## Prerequisites

- A public RPC for Base mainnet (chain ID 8453) or Base Sepolia (chain ID 84532)
- `viem` installed (`npm install viem`)
- An `address` to query for user-specific reads (can be `zeroAddress` if not needed)

## Addresses

Base mainnet (chain ID 8453):

```ts
const JACKPOT_ADDRESS           = '0x3bAe643002069dBCbcd62B1A4eb4C4A397d042a2' as const;
const PAYOUT_CALC_ADDRESS       = '0x97a22361b6208aC8cd9afaea09D20feC47046CBD' as const;
const LP_MANAGER_ADDRESS        = '0xE63E54DF82d894396B885CE498F828f2454d9dCf' as const;
const TICKET_NFT_ADDRESS        = '0x48FfE35AbB9f4780a4f1775C2Ce1c46185b366e4' as const;
const BATCH_FACILITATOR_ADDRESS = '0x01774B531591b286b9f02C6Bc02ab3fD9526Aa76' as const;
const JACKPOT_AUTO_SUBSCRIPTION = '0x02A58B725116BA687D9356Eafe0fA771d58a37ac' as const;
```

Base Sepolia testnet (chain ID 84532):

```ts
const JACKPOT_ADDRESS           = '0x465dA3c859f193A3807386387bEE941B2A4c3279' as const;
const PAYOUT_CALC_ADDRESS       = '0xE9542aC6FaDC47be2Bc42Fc075c1f481529D28cB' as const;
const LP_MANAGER_ADDRESS        = '0x36408921aB820305F109150003C0F90aE1CB1766' as const;
const TICKET_NFT_ADDRESS        = '0x45084829ac63f9dC6a3D4981A46FA896f9180ECd' as const;
const BATCH_FACILITATOR_ADDRESS = '0xe582dD908Ca5bd51C743DFdda37C93bBaCD27c56' as const;
const JACKPOT_AUTO_SUBSCRIPTION = '0x054a61E2FC77BAb3c9D94C3f835FB7ADE97a2F90' as const;
```

> Base mainnet public RPC: `https://mainnet.base.org` (rate-limited; use [Alchemy](https://www.alchemy.com/) or [QuickNode](https://www.quicknode.com/) for production).

## ABI Fragments

Only the fragments needed for this task:

```ts
import { parseAbi } from 'viem';

const jackpotAbi = parseAbi([
  // Drawing state
  "function currentDrawingId() view returns (uint256)",
  "function getDrawingState(uint256 _drawingId) view returns ((uint256 prizePool, uint256 ticketPrice, uint256 edgePerTicket, uint256 referralWinShare, uint256 referralFee, uint256 globalTicketsBought, uint256 lpEarnings, uint256 drawingTime, uint256 winningTicket, uint8 ballMax, uint8 bonusballMax, address payoutCalculator, bool jackpotLock))",
  // Config constants (protocol-level parameters — see note below on bonusball pick range)
  "function ticketPrice() view returns (uint256)",
  "function normalBallMax() view returns (uint8)",
  "function bonusballMin() view returns (uint8)",      // protocol config, NOT the pick lower bound
  "function bonusballHardCap() view returns (uint8)",  // protocol config, NOT the pick upper bound
  "function drawingDurationInSeconds() view returns (uint256)",
  "function maxReferrers() view returns (uint256)",
  // User referral balance
  "function referralFees(address) view returns (uint256)",
  // Ticket tier check (see megapot-claim-winnings for full claim flow)
  "function getTicketTierIds(uint256[] _ticketIds) view returns (uint256[] tierIds)",
]);

const payoutCalcAbi = parseAbi([
  "function getExpectedDrawingTierPayouts(uint256 _drawingId, uint256 _prizePool, uint8 _normalMax, uint8 _bonusballMax) view returns (uint256[12] drawingTierPayouts)",
]);

const lpManagerAbi = parseAbi([
  "function getLpInfo(address _lpAddress) view returns ((uint256 consolidatedShares, (uint256 amount, uint256 drawingId) lastDeposit, (uint256 amountInShares, uint256 drawingId) pendingWithdrawal, uint256 claimableWithdrawals))",
  "function getLPValueBreakdown(address _lpAddress) view returns ((uint256 activeDeposits, uint256 pendingDeposits, uint256 pendingWithdrawals, uint256 claimableWithdrawals) breakdown)",
  "function lpPoolCap() view returns (uint256)",
  "function getLPDrawingState(uint256 _drawingId) view returns ((uint256 lpPoolTotal, uint256 pendingDeposits, uint256 pendingWithdrawals))",
]);

const ticketNftAbi = parseAbi([
  "function getUserTickets(address _userAddress, uint256 _drawingId) view returns ((uint256 ticketId, (uint256 drawingId, uint256 packedTicket, bytes32 referralScheme) ticket, uint8[] normals, uint8 bonusball)[])",
]);

const batchFacilitatorAbi = parseAbi([
  "function getBatchOrderInfo(address _recipient) view returns (((uint256 orderDrawingId, uint64 remainingUSDC, uint64 remainingTickets, uint64 totalTicketsOrdered, uint64 dynamicTicketCount, address[] referrers, uint256[] referralSplit) batchOrder, (uint8[] normals, uint8 bonusball)[] staticTickets))",
]);

const autoSubAbi = parseAbi([
  "function getSubscriptionInfo(address _recipient) view returns (((uint64 remainingUSDC, uint64 lastExecutedDrawing, uint64 subscribedTicketPrice, uint64 dynamicTicketCount, address[] referrers, uint256[] referralSplit) subscription, (uint8[] normals, uint8 bonusball)[] staticTickets))",
]);
```

## Recipe

### Setup

```ts
import { createPublicClient, http, parseAbi } from 'viem';
import { base } from 'viem/chains';

// --- PLACEHOLDER: replace with the address you want to query ---
const USER_ADDRESS = '0xYOUR_WALLET_ADDRESS' as `0x${string}`; // placeholder

const RPC_URL = 'https://mainnet.base.org'; // or your own RPC URL

// For Base Sepolia testnet: replace `base` with `baseSepolia` from 'viem/chains',
// swap addresses to the testnet values above, and use https://sepolia.base.org as RPC.

// Base mainnet addresses
const JACKPOT_ADDRESS           = '0x3bAe643002069dBCbcd62B1A4eb4C4A397d042a2' as const;
const PAYOUT_CALC_ADDRESS       = '0x97a22361b6208aC8cd9afaea09D20feC47046CBD' as const;
const LP_MANAGER_ADDRESS        = '0xE63E54DF82d894396B885CE498F828f2454d9dCf' as const;
const TICKET_NFT_ADDRESS        = '0x48FfE35AbB9f4780a4f1775C2Ce1c46185b366e4' as const;
const BATCH_FACILITATOR_ADDRESS = '0x01774B531591b286b9f02C6Bc02ab3fD9526Aa76' as const;
const JACKPOT_AUTO_SUBSCRIPTION = '0x02A58B725116BA687D9356Eafe0fA771d58a37ac' as const;

const jackpotAbi = parseAbi([
  "function currentDrawingId() view returns (uint256)",
  "function getDrawingState(uint256 _drawingId) view returns ((uint256 prizePool, uint256 ticketPrice, uint256 edgePerTicket, uint256 referralWinShare, uint256 referralFee, uint256 globalTicketsBought, uint256 lpEarnings, uint256 drawingTime, uint256 winningTicket, uint8 ballMax, uint8 bonusballMax, address payoutCalculator, bool jackpotLock))",
  "function ticketPrice() view returns (uint256)",
  "function normalBallMax() view returns (uint8)",
  "function bonusballMin() view returns (uint8)",      // protocol config, NOT the pick lower bound
  "function bonusballHardCap() view returns (uint8)",  // protocol config, NOT the pick upper bound
  "function drawingDurationInSeconds() view returns (uint256)",
  "function maxReferrers() view returns (uint256)",
  "function referralFees(address) view returns (uint256)",
  "function getTicketTierIds(uint256[] _ticketIds) view returns (uint256[] tierIds)",
]);

const payoutCalcAbi = parseAbi([
  "function getExpectedDrawingTierPayouts(uint256 _drawingId, uint256 _prizePool, uint8 _normalMax, uint8 _bonusballMax) view returns (uint256[12] drawingTierPayouts)",
]);

const lpManagerAbi = parseAbi([
  "function getLpInfo(address _lpAddress) view returns ((uint256 consolidatedShares, (uint256 amount, uint256 drawingId) lastDeposit, (uint256 amountInShares, uint256 drawingId) pendingWithdrawal, uint256 claimableWithdrawals))",
  "function getLPValueBreakdown(address _lpAddress) view returns ((uint256 activeDeposits, uint256 pendingDeposits, uint256 pendingWithdrawals, uint256 claimableWithdrawals) breakdown)",
  "function lpPoolCap() view returns (uint256)",
  "function getLPDrawingState(uint256 _drawingId) view returns ((uint256 lpPoolTotal, uint256 pendingDeposits, uint256 pendingWithdrawals))",
]);

const ticketNftAbi = parseAbi([
  "function getUserTickets(address _userAddress, uint256 _drawingId) view returns ((uint256 ticketId, (uint256 drawingId, uint256 packedTicket, bytes32 referralScheme) ticket, uint8[] normals, uint8 bonusball)[])",
]);

const batchFacilitatorAbi = parseAbi([
  "function getBatchOrderInfo(address _recipient) view returns (((uint256 orderDrawingId, uint64 remainingUSDC, uint64 remainingTickets, uint64 totalTicketsOrdered, uint64 dynamicTicketCount, address[] referrers, uint256[] referralSplit) batchOrder, (uint8[] normals, uint8 bonusball)[] staticTickets))",
]);

const autoSubAbi = parseAbi([
  "function getSubscriptionInfo(address _recipient) view returns (((uint64 remainingUSDC, uint64 lastExecutedDrawing, uint64 subscribedTicketPrice, uint64 dynamicTicketCount, address[] referrers, uint256[] referralSplit) subscription, (uint8[] normals, uint8 bonusball)[] staticTickets))",
]);

const publicClient = createPublicClient({
  chain: base,
  transport: http(RPC_URL),
});
```

### Round 1 — Fetch drawing ID, then drawing state

`getExpectedDrawingTierPayouts` requires `prizePool`, `ballMax`, and `bonusballMax` from the current drawing state. Fetch those first so they can be passed as real values into Round 2.

```ts
// Step 1a: get the current drawing ID
const currentDrawingId = await publicClient.readContract({
  address: JACKPOT_ADDRESS,
  abi: jackpotAbi,
  functionName: 'currentDrawingId',
});
// e.g. 42n

// Step 1b: get drawing state — provides prizePool / ballMax / bonusballMax for the payout calculator
const drawingState = await publicClient.readContract({
  address: JACKPOT_ADDRESS,
  abi: jackpotAbi,
  functionName: 'getDrawingState',
  args: [currentDrawingId],
});

const { prizePool, ballMax, bonusballMax } = drawingState;
```

### Round 2 — Batch all remaining reads in a single multicall

Now that `prizePool`, `ballMax`, and `bonusballMax` are known, pass them directly into `getExpectedDrawingTierPayouts` alongside every other read.

```ts
const [
  tierPayouts,
  ticketPrice,
  normalBallMax,
  bonusballMin,
  bonusballHardCap,
  drawingDurationInSeconds,
  maxReferrers,
  referralBalance,
  userTickets,
  lpInfo,
  lpValueBreakdown,
  lpPoolCap,
  lpDrawingState,
  batchOrderInfo,
  subscriptionInfo,
] = await publicClient.multicall({
  allowFailure: false,
  contracts: [
    // --- Expected prize tier payouts (12 tiers) — real values from Round 1 ---
    {
      address: PAYOUT_CALC_ADDRESS,
      abi: payoutCalcAbi,
      functionName: 'getExpectedDrawingTierPayouts',
      args: [currentDrawingId, prizePool, ballMax, bonusballMax],
    },
    // --- Config constants ---
    {
      address: JACKPOT_ADDRESS,
      abi: jackpotAbi,
      functionName: 'ticketPrice',
    },
    {
      address: JACKPOT_ADDRESS,
      abi: jackpotAbi,
      functionName: 'normalBallMax',
    },
    {
      address: JACKPOT_ADDRESS,
      abi: jackpotAbi,
      functionName: 'bonusballMin',
    },
    {
      address: JACKPOT_ADDRESS,
      abi: jackpotAbi,
      functionName: 'bonusballHardCap',
    },
    {
      address: JACKPOT_ADDRESS,
      abi: jackpotAbi,
      functionName: 'drawingDurationInSeconds',
    },
    {
      address: JACKPOT_ADDRESS,
      abi: jackpotAbi,
      functionName: 'maxReferrers',
    },
    // --- User: referral fees accrued ---
    {
      address: JACKPOT_ADDRESS,
      abi: jackpotAbi,
      functionName: 'referralFees',
      args: [USER_ADDRESS],
    },
    // --- User: tickets in current drawing ---
    // Included here so on-connect rendering is one round-trip. For a
    // tickets-only surface (no LP / no batch order / no subscription),
    // prefer GET /v1/wallets/{addr}/tickets/rounds/{currentDrawingId}
    // from the Data API — see the matrix at the top of this skill and
    // `megapot-data-api`.
    {
      address: TICKET_NFT_ADDRESS,
      abi: ticketNftAbi,
      functionName: 'getUserTickets',
      args: [USER_ADDRESS, currentDrawingId],
    },
    // --- User: LP position (share counts + pending movements) ---
    {
      address: LP_MANAGER_ADDRESS,
      abi: lpManagerAbi,
      functionName: 'getLpInfo',
      args: [USER_ADDRESS],
    },
    // --- User: LP USD value breakdown ---
    {
      address: LP_MANAGER_ADDRESS,
      abi: lpManagerAbi,
      functionName: 'getLPValueBreakdown',
      args: [USER_ADDRESS],
    },
    // --- Pool stats: LP pool cap ---
    {
      address: LP_MANAGER_ADDRESS,
      abi: lpManagerAbi,
      functionName: 'lpPoolCap',
    },
    // --- Pool stats: LP drawing state (total, pending deposits/withdrawals) ---
    {
      address: LP_MANAGER_ADDRESS,
      abi: lpManagerAbi,
      functionName: 'getLPDrawingState',
      args: [currentDrawingId],
    },
    // --- User: active batch order (remainingTickets, remainingUSDC, staticTickets) ---
    {
      address: BATCH_FACILITATOR_ADDRESS,
      abi: batchFacilitatorAbi,
      functionName: 'getBatchOrderInfo',
      args: [USER_ADDRESS],
    },
    // --- User: active subscription (remainingUSDC, lastExecutedDrawing, etc.) ---
    {
      address: JACKPOT_AUTO_SUBSCRIPTION,
      abi: autoSubAbi,
      functionName: 'getSubscriptionInfo',
      args: [USER_ADDRESS],
    },
  ],
});
```

### Consume the results

```ts
console.log('=== Drawing ===');
console.log('Drawing ID:', currentDrawingId);
console.log('Prize pool (USDC 6 dec):', drawingState.prizePool);
console.log('Ticket price (USDC 6 dec):', drawingState.ticketPrice);
console.log('Tickets sold:', drawingState.globalTicketsBought);
console.log('Drawing time (unix):', drawingState.drawingTime);
console.log('Jackpot locked:', drawingState.jackpotLock);

console.log('=== Config ===');
console.log('Ticket price:', ticketPrice);
console.log('Normal ball max (pick range [1, normalBallMax]):', normalBallMax);  // e.g. 30
// bonusballMin / bonusballHardCap are protocol config parameters used internally to set
// each drawing's bonusballMax — they are NOT the user-facing pick range.
// The actual bonusball pick range is [1, drawingState.bonusballMax] (from Round 1 above).
console.log('Bonusball pick range: [1,', bonusballMax, ']');  // use drawingState.bonusballMax
console.log('Bonus ball min (protocol config):', bonusballMin);
console.log('Bonus ball hard cap (protocol config):', bonusballHardCap);
console.log('Drawing duration (s):', drawingDurationInSeconds);
console.log('Max referrers:', maxReferrers);

console.log('=== Prize Tiers ===');
tierPayouts.forEach((payout, i) => {
  console.log(`Tier ${i} payout (USDC 6 dec):`, payout);
});

console.log('=== User ===');
console.log('Referral fees claimable (USDC 6 dec):', referralBalance);
console.log('Tickets in current drawing:', userTickets.length);
userTickets.forEach(({ ticketId, normals, bonusball }) => {
  console.log(`  Ticket #${ticketId}: normals=${normals}, bonusball=${bonusball}`);
});

console.log('=== LP Position ===');
console.log('Consolidated shares:', lpInfo.consolidatedShares);
console.log('Last deposit amount:', lpInfo.lastDeposit.amount);
console.log('Pending withdrawal (shares):', lpInfo.pendingWithdrawal.amountInShares);
console.log('Claimable withdrawals (USDC 6 dec):', lpInfo.claimableWithdrawals);
console.log('Active deposits (USDC 6 dec):', lpValueBreakdown.activeDeposits);
console.log('Pending deposits (USDC 6 dec):', lpValueBreakdown.pendingDeposits);

console.log('=== Pool Stats ===');
console.log('LP pool cap (USDC 6 dec):', lpPoolCap);
console.log('LP pool total (USDC 6 dec):', lpDrawingState.lpPoolTotal);
console.log('LP pending deposits:', lpDrawingState.pendingDeposits);
console.log('LP pending withdrawals:', lpDrawingState.pendingWithdrawals);

console.log('=== Batch Order ===');
const { batchOrder, staticTickets: batchStaticTickets } = batchOrderInfo;
console.log('Remaining tickets:', batchOrder.remainingTickets);
console.log('Remaining USDC:', batchOrder.remainingUSDC);
console.log('Static tickets:', batchStaticTickets.length);

console.log('=== Subscription ===');
const { subscription, staticTickets: subStaticTickets } = subscriptionInfo;
console.log('Remaining USDC:', subscription.remainingUSDC);
console.log('Last executed drawing:', subscription.lastExecutedDrawing);
console.log('Subscribed ticket price:', subscription.subscribedTicketPrice);
console.log('Dynamic ticket count per drawing:', subscription.dynamicTicketCount);
console.log('Static tickets per drawing:', subStaticTickets.length);
```

## Return Types Reference

### `getDrawingState` struct fields

| Field | Type | Description |
|---|---|---|
| `prizePool` | `uint256` | Current prize pool in USDC (6 decimals) |
| `ticketPrice` | `uint256` | Ticket price for this drawing |
| `edgePerTicket` | `uint256` | LP edge per ticket in USDC (6 decimals); set at drawing init as `lpEdgeTarget * ticketPrice / 1e18` |
| `referralWinShare` | `uint256` | Share of winnings going to referrers (1e18 scale) |
| `referralFee` | `uint256` | Referral purchase fee rate (1e18 scale, percentage of ticket price) |
| `globalTicketsBought` | `uint256` | Total tickets sold in this drawing |
| `lpEarnings` | `uint256` | **Gross ticket revenue routed to the LP side** for this drawing (USDC, 6 decimals). Equals `Σ (ticketsValue − referralFeeTotal)` over all purchases in the drawing. **NOT net LP yield** — see "Net LP yield (paid out)" in the Drawing Statistics Catalog below. |
| `drawingTime` | `uint256` | Unix timestamp when the drawing can be run |
| `winningTicket` | `uint256` | Packed winning ticket (0 if not yet drawn) |
| `ballMax` | `uint8` | Normal ball upper bound for this drawing |
| `bonusballMax` | `uint8` | Bonus ball upper bound for this drawing |
| `payoutCalculator` | `address` | Payout calculator contract for this drawing |
| `jackpotLock` | `bool` | True when the jackpot is locked ahead of a draw |

### `getExpectedDrawingTierPayouts` return

`uint256[12]` — 12 prize tier payouts in USDC (6 decimals). Index N is the payout for tier N (see Prize Tier System below). Indices 0 and 2 are always `0` because those tiers do not pay out.

### `getLpInfo` struct fields

| Field | Type | Description |
|---|---|---|
| `consolidatedShares` | `uint256` | Current LP share balance |
| `lastDeposit.amount` | `uint256` | Amount of the most recent deposit |
| `lastDeposit.drawingId` | `uint256` | Drawing when the last deposit was made |
| `pendingWithdrawal.amountInShares` | `uint256` | Shares queued for withdrawal |
| `pendingWithdrawal.drawingId` | `uint256` | Drawing when withdrawal was initiated |
| `claimableWithdrawals` | `uint256` | USDC ready to be claimed (6 decimals) |

### `getBatchOrderInfo` inner struct fields

| Field | Type | Description |
|---|---|---|
| `orderDrawingId` | `uint256` | Drawing the batch order was created for |
| `remainingUSDC` | `uint64` | USDC left in the order (6 decimals) |
| `remainingTickets` | `uint64` | Tickets yet to be purchased |
| `totalTicketsOrdered` | `uint64` | Total tickets when order was placed |
| `dynamicTicketCount` | `uint64` | Random-pick tickets per execution |
| `referrers` | `address[]` | Referrer list |
| `referralSplit` | `uint256[]` | Fee split in bps per referrer |

### `getSubscriptionInfo` inner struct fields

| Field | Type | Description |
|---|---|---|
| `remainingUSDC` | `uint64` | USDC balance remaining in subscription |
| `lastExecutedDrawing` | `uint64` | Last drawing this subscription executed for |
| `subscribedTicketPrice` | `uint64` | Ticket price at subscription creation time |
| `dynamicTicketCount` | `uint64` | Random-pick tickets per drawing |
| `referrers` | `address[]` | Referrer list |
| `referralSplit` | `uint256[]` | Fee split in bps per referrer |

## Notes

- `getBatchOrderInfo` and `getSubscriptionInfo` revert with `NoActiveBatchOrder` / `NoActiveSubscription` when no order/subscription exists. Wrap those calls in `allowFailure: true` (and check the `error` field on the result) if the user may not have an active order.
- `getUserTickets` returns an empty array (not a revert) when the user has no tickets in the requested drawing.
- All USDC amounts are in 6-decimal units (1 USDC = 1_000_000n).
- `drawingTime` is a Unix timestamp; compare to `Date.now() / 1000` to determine time remaining.

## Handling Optional Calls (allowFailure variant)

```ts
const results = await publicClient.multicall({
  allowFailure: true, // individual calls may fail
  contracts: [
    {
      address: BATCH_FACILITATOR_ADDRESS,
      abi: batchFacilitatorAbi,
      functionName: 'getBatchOrderInfo',
      args: [USER_ADDRESS],
    },
    {
      address: JACKPOT_AUTO_SUBSCRIPTION,
      abi: autoSubAbi,
      functionName: 'getSubscriptionInfo',
      args: [USER_ADDRESS],
    },
  ],
});

const batchResult = results[0].status === 'success' ? results[0].result : null;
const subResult   = results[1].status === 'success' ? results[1].result : null;

if (!batchResult) console.log('No active batch order');
if (!subResult)   console.log('No active subscription');
```

## Prize Tier System

The Megapot lottery uses a 12-tier prize system. The tier ID formula is:

```
tierId = normalMatches * 2 + bonusballMatch
```

where `bonusballMatch` is `1` if the bonusball matched, `0` otherwise.

| Tier ID | Normal Matches | Bonusball | Payout? | Description |
|---|---|---|---|---|
| 0 | 0 | No | No | No match |
| 1 | 0 | Yes | Yes | Bonusball only |
| 2 | 1 | No | No | 1 match, no payout |
| 3 | 1 | Yes | Yes | 1 match + bonusball |
| 4 | 2 | No | Yes | 2 matches |
| 5 | 2 | Yes | Yes | 2 matches + bonusball |
| 6 | 3 | No | Yes | 3 matches |
| 7 | 3 | Yes | Yes | 3 matches + bonusball |
| 8 | 4 | No | Yes | 4 matches |
| 9 | 4 | Yes | Yes | 4 matches + bonusball |
| 10 | 5 | No | Yes | 5 matches (grand prize runner-up) |
| 11 | 5 | Yes | Yes | **Jackpot** (5 matches + bonusball) |

`getExpectedDrawingTierPayouts` returns `uint256[12]` where index N is the USDC payout (6 decimals) for tier N. Indices 0 and 2 will always be `0` because those tiers do not pay out. When reading `getTicketTierIds`, a result of `0n` or `2n` means the ticket is not a winner.

**Payout interpretation:** Each value in the `uint256[12]` array from `getExpectedDrawingTierPayouts` is the **per-ticket** payout for that tier in USDC (6 decimals). If 3 tickets match tier 6, each ticket independently receives the tier-6 payout amount — it is NOT split among winners.

**Check ticket winners:** To look up tier results for specific ticket IDs, call `getTicketTierIds` on the Jackpot contract. For the full claiming flow, see `megapot-claim-winnings`.

## Drawing Lifecycle UX & Events

The drawing lifecycle is the protocol's most distinctive on-chain UX moment. This section maps each lifecycle state to a detection predicate, the events to subscribe to, and recommended UI/polling cadence so a homepage can render the lifecycle live.

### State machine

Each state is detected from `getDrawingState(drawingId)` plus `currentDrawingId()`:

| State | Detection (one-shot) | Polling cadence | UI guidance |
|---|---|---|---|
| Open | `id === currentDrawingId() && drawingTime > now && !jackpotLock` | 30s | Countdown to `drawingTime`; "Buy tickets" CTA enabled |
| DrawingTimeReached | `id === currentDrawingId() && drawingTime <= now && !jackpotLock` | 5s | "Drawing time reached — settling soon"; disable buy CTA |
| SettlementRequested | `id === currentDrawingId() && jackpotLock && winningTicket === 0n` | 5s | "Settling…" spinner; entropy callback in flight |
| Settled | `winningTicket !== 0n` (state lives at `currentDrawingId() - 1n` once a new drawing is initialized) | event subscription | "Drawing N results" with winning numbers |
| ClaimingOpen | Settled AND user owns winning tickets for that drawing | on user navigation | "Claim winnings" CTA |

The transition into the next drawing fires `NewDrawingInitialized`; `currentDrawingId()` increments at that point, and the just-settled drawing's state is then read at `currentDrawingId() - 1n`.

### Event timeline

Each state transition fires zero or more events. Subscribe to the "Events emitted" column to drive UI updates:

| Transition | Trigger | Events emitted (in order) |
|---|---|---|
| (drawing init) → Open | A previous drawing settled, or initial deploy | `NewDrawingInitialized(drawingId, …)` |
| Open → DrawingTimeReached | `block.timestamp >= drawingTime` | (none — timestamp-only; UI must detect via polling/clock) |
| DrawingTimeReached → SettlementRequested | A keeper calls `Jackpot.runJackpot()` | `JackpotLocked(drawingId)`, then `JackpotRunRequested(drawingId, entropyGasLimit, fee)` |
| SettlementRequested → Settled | Pyth entropy callback fulfills the request (1–2 blocks later) | `WinnersCalculated(drawingId, …)`, then `JackpotSettled(drawingId, …)`, then `NewDrawingInitialized(drawingId + 1, …)` for the next drawing |
| Settled → ClaimingOpen | Per-user — depends on whether the user owns a winning ticket NFT | (none — per-user check via `getTicketTierIds([ticketIds])`) |

**Failure path:** if a settlement attempt is reverted (e.g. the entropy callback fails), `JackpotUnlocked(drawingId)` fires. Treat this as a return to the prior state with `jackpotLock` cleared and `winningTicket` still `0n`; a fresh `runJackpot()` call is required to retry.

### Events

Events are emitted from three different contracts. When subscribing
(e.g. `useWatchContractEvent` in wagmi, `getLogs` in viem), the
`address` argument must match the emitting contract — wiring every
event to `JACKPOT_ADDRESS` silently produces no logs for the
BatchPurchaseFacilitator and JackpotAutoSubscription events.

| Event | Emitted by | Subscribe `address` to |
|---|---|---|
| `JackpotLocked`, `JackpotUnlocked`, `JackpotRunRequested`, `WinnersCalculated`, `JackpotSettled`, `NewDrawingInitialized` | Jackpot | `JACKPOT_ADDRESS` |
| `TicketPurchased` | Jackpot | `JACKPOT_ADDRESS` |
| `BatchOrderExecuted` | BatchPurchaseFacilitator | `BATCH_FACILITATOR_ADDRESS` |
| `SubscriptionExecuted` | JackpotAutoSubscription | `JACKPOT_AUTO_SUBSCRIPTION` |

```ts
import { parseAbi } from 'viem';

const lifecycleAbi = parseAbi([
  // Lifecycle (Jackpot — subscribe via JACKPOT_ADDRESS)
  "event JackpotLocked(uint256 indexed drawingId)",
  "event JackpotUnlocked(uint256 indexed drawingId)",
  "event JackpotRunRequested(uint256 indexed drawingId, uint256 entropyGasLimit, uint256 fee)",
  "event WinnersCalculated(uint256 indexed drawingId, uint256[] winningNormals, uint256 winningBonusball, uint256[] uniqueResult, uint256[] dupResult)",
  "event JackpotSettled(uint256 indexed drawingId, uint256 lpEarnings, uint256 userWinnings, uint8 winningBonusball, uint256 winningNumbers, uint256 newDrawingAccumulator)",
  "event NewDrawingInitialized(uint256 indexed drawingId, uint256 lpPoolTotal, uint256 prizePool, uint256 ticketPrice, uint256 normalBallMax, uint8 bonusballMax, uint256 referralWinShare, uint256 drawingTime)",

  // Activity (Jackpot — subscribe via JACKPOT_ADDRESS)
  "event TicketPurchased(address indexed recipient, uint256 indexed currentDrawingId, bytes32 indexed source, uint256 userTicketId, uint8[] normals, uint8 bonusball, bytes32 referralScheme)",

  // Activity (BatchPurchaseFacilitator — subscribe via BATCH_FACILITATOR_ADDRESS)
  "event BatchOrderExecuted(address indexed user, uint256 indexed drawingId, uint256[] ticketIds, uint256 ticketsExecuted, uint256 remainingTickets, uint256 remainingUSDC)",

  // Activity (JackpotAutoSubscription — subscribe via JACKPOT_AUTO_SUBSCRIPTION)
  "event SubscriptionExecuted(address indexed recipient, uint256 indexed drawingId, uint256[] ticketIds, uint256 dynamicTicketsPurchased, uint256 staticTicketsPurchased)",
]);
```

### Lifecycle event fields

#### `JackpotLocked(uint256 indexed drawingId)`

| Field | Description |
|---|---|
| `drawingId` | Drawing being locked for settlement. Same as `currentDrawingId()` at the time `runJackpot()` was called. |

#### `JackpotUnlocked(uint256 indexed drawingId)`

| Field | Description |
|---|---|
| `drawingId` | Drawing whose lock was reverted. Settlement did not complete; the drawing returns to its prior state and a fresh `runJackpot()` call is required to retry. |

#### `JackpotRunRequested(uint256 indexed drawingId, uint256 entropyGasLimit, uint256 fee)`

| Field | Description |
|---|---|
| `drawingId` | Drawing for which entropy was requested. |
| `entropyGasLimit` | Gas allocated to the Pyth entropy callback. |
| `fee` | Wei paid to Pyth for the entropy callback (overpayment is refunded to the caller). Read live via `Jackpot.getEntropyCallbackFee()`. |

#### `WinnersCalculated(uint256 indexed drawingId, uint256[] winningNormals, uint256 winningBonusball, uint256[] uniqueResult, uint256[] dupResult)`

| Field | Description |
|---|---|
| `drawingId` | The drawing being settled. |
| `winningNormals` | The 5 winning normal-ball values, unpacked. |
| `winningBonusball` | The winning bonusball value (1..bonusballMax). |
| `uniqueResult` | `uint256[12]` — count of unique-combo winners at each tier (index = `tierId`). Tier indices 0 and 2 are always `0n`. |
| `dupResult` | `uint256[12]` — count of duplicate-combo winners at each tier. Total winners at tier `i` = `uniqueResult[i] + dupResult[i]`. See "Drawing Statistics Catalog" below for the per-tier-payout pairing. |

#### `JackpotSettled(uint256 indexed drawingId, uint256 lpEarnings, uint256 userWinnings, uint8 winningBonusball, uint256 winningNumbers, uint256 newDrawingAccumulator)`

| Field | Description |
|---|---|
| `drawingId` | The just-settled drawing. |
| `lpEarnings` | **Gross** USDC routed to LP side for this drawing (6 decimals) — equal to `getDrawingState(drawingId).lpEarnings` at settlement. **NOT net LP yield.** To compute net LP yield, subtract `userWinnings` and the matching `ProtocolFeeCollected.amount` for this `drawingId`. |
| `userWinnings` | Total USDC distributed to ticket winners across all tiers (6 decimals). |
| `winningBonusball` | The drawn bonusball value (1..bonusballMax). |
| `winningNumbers` | Packed `uint256` of the winning ticket. Decode with `Jackpot.getUnpackedTicket(drawingId, winningNumbers)`. |
| `newDrawingAccumulator` | Carry-over accumulator value seeding the next drawing. |

#### `NewDrawingInitialized(uint256 indexed drawingId, uint256 lpPoolTotal, uint256 prizePool, uint256 ticketPrice, uint256 normalBallMax, uint8 bonusballMax, uint256 referralWinShare, uint256 drawingTime)`

| Field | Description |
|---|---|
| `drawingId` | The newly-opened drawing. After this fires, `currentDrawingId() === drawingId`. |
| `lpPoolTotal` | LP USDC pool size at drawing start (6 decimals). |
| `prizePool` | Starting prize pool for this drawing (6 decimals). |
| `ticketPrice` | Locked ticket price for the drawing (6 decimals). |
| `normalBallMax` | Normal-ball pick upper bound for this drawing. |
| `bonusballMax` | Bonusball pick upper bound for this drawing. |
| `referralWinShare` | Referrer share of winnings (1e18 scale). |
| `drawingTime` | Unix timestamp when ticket purchases close and settlement becomes eligible. |

### Activity event fields

For per-event field semantics of `TicketPurchased`, `BatchOrderExecuted`, and `SubscriptionExecuted`, see the corresponding write-side skills:

- `TicketPurchased` — `megapot-buy-tickets`
- `BatchOrderExecuted` — `megapot-buy-bulk`
- `SubscriptionExecuted` — `megapot-subscribe`

### Subscription matrix

| UI need | Subscribe to | Read on connect |
|---|---|---|
| Homepage countdown / "drawing now" / "settling" | `JackpotLocked`, `JackpotRunRequested`, `JackpotSettled`, `NewDrawingInitialized` | `getDrawingState(currentDrawingId())` |
| Live ticket-sales counter for current drawing | `TicketPurchased`, `BatchOrderExecuted`, `SubscriptionExecuted` | `getDrawingState(currentDrawingId()).globalTicketsBought` |
| Recently-settled drawing results | `JackpotSettled` | `getDrawingState(currentDrawingId() - 1n)` |

### Latency

- `JackpotLocked` → `JackpotSettled` is 1–2 blocks under normal Pyth conditions; allow up to ~30s in pathological cases.
- `runJackpot()` is `payable` and anyone can call it. The caller pays the Pyth entropy fee, refunded for overpayment. `Jackpot.getEntropyCallbackFee()` returns the current fee. Frontends never need to call this — keepers do.

### Polling vs subscriptions

- Default to polling `getDrawingState(currentDrawingId())` because it's the source of truth for `jackpotLock` and `winningTicket` regardless of whether you missed an event.
- Use event subscriptions as a low-latency *supplement* — when `JackpotSettled` fires, refresh state immediately rather than waiting for the next 5s tick.

## Enumerating Past Drawings

To render a "Drawing History" page, batch reads of past `getDrawingState(id)` calls in a single multicall round. Pages are sized with a cursor that walks backward from `currentDrawingId() - 1n`.

> **Off-chain alternative:** for >100 drawings, any cross-drawing aggregation, or any history page that ships to public users, prefer `GET /v1/rounds?cursor=…` from the Data API — it's already cursor-paginated, returns settled-round summaries, and survives a missing public RPC. The multicall recipe below is the right path for keepers, agents, or non-React surfaces that need fresh state without a Data API roundtrip. See `megapot-data-api` and the "Threshold guidance" subsection at the bottom of this section for the exact threshold ranges.

### Recipe

```ts
const currentId = await publicClient.readContract({
  address: JACKPOT_ADDRESS,
  abi: jackpotAbi,
  functionName: 'currentDrawingId',
});

const PAGE_SIZE = 10n;
const startId = currentId - 1n;                                  // most recent settled
const endId   = startId > PAGE_SIZE ? startId - PAGE_SIZE + 1n : 1n;

const calls = [];
for (let id = startId; id >= endId; id--) {
  calls.push({
    address: JACKPOT_ADDRESS,
    abi: jackpotAbi,
    functionName: 'getDrawingState',
    args: [id],
  });
}

const drawingStates = await publicClient.multicall({
  allowFailure: false,
  contracts: calls,
});

// Next-page cursor — null when nothing left
const nextCursor = endId > 1n ? endId - 1n : null;
```

### Threshold guidance

- **≤20 drawings:** direct multicall is fine. RPC bandwidth is acceptable for a History page first paint.
- **21–100 drawings:** still feasible via multicall but consider lazy-loading per-row body (e.g. winners count) on expand.
- **>100 drawings or any aggregation across all drawings:** use the **Data API** (`GET /v1/rounds` cursor-paginated; `megapot-data-api` skill). Direct RPC reads or self-hosted indexers are no longer needed for these aggregates.

See "Drawing Statistics Catalog" below for which fields require off-chain aggregation (Data API) regardless of drawing count.

## Drawing Statistics Catalog

The canonical set of "drawing statistics" an integrator might display, with the source and computation for each.

> **Off-chain alternative:** any row below marked `No — see megapot-data-api` (or any cross-drawing aggregate over more than one row) requires the Data API — RPC scans across the full drawing history are not viable for production homepages. The catalog still documents the on-chain source so keepers and agents can compute the same numbers locally; UI surfaces should reach for the API endpoint named in the row instead. See `megapot-data-api`.

### Catalog

| Stat | Scope | Source | Computation | RPC-only? |
|---|---|---|---|---|
| Tickets sold | per-drawing | `getDrawingState(id).globalTicketsBought` | direct read | Yes |
| Prize pool | per-drawing | `getDrawingState(id).prizePool` | direct read | Yes |
| Gross LP revenue (pre-settlement) | per-drawing | `getDrawingState(id).lpEarnings` | direct read — **gross revenue, not yield** | Yes |
| Net LP yield (paid out to pool) | per-drawing (settled) | `JackpotSettled.lpEarnings − JackpotSettled.userWinnings − ProtocolFeeCollected.amount` (matched on `drawingId`) | event log scan | event log scan |
| LP pool delta | per-drawing (settled) | `getLPDrawingState(id+1).lpPoolTotal − getLPDrawingState(id).lpPoolTotal`, adjusted for LP deposits/withdrawals processed at settlement | direct read | Yes |
| Drawing time | per-drawing | `getDrawingState(id).drawingTime` | direct read (Unix seconds) | Yes |
| Winners per tier | per-drawing (settled) | `WinnersCalculated` event filtered by `drawingId` | read `uniqueResult[]` and `dupResult[]` arrays | event log scan |
| Total user winnings | per-drawing (settled) | `JackpotSettled.userWinnings` filtered by `drawingId` | direct field on the event | event log scan |
| Settlement latency | per-drawing (settled) | `JackpotLocked` + `JackpotSettled` events | block delta between matching `drawingId` events | event log scan |
| Cumulative drawings completed | protocol | `currentDrawingId() - 1n` | direct read | Yes |
| Cumulative ticket volume | protocol | Data API (`globalTicketsBought × ticketPrice` summed across all drawings — `GET /v1/rounds`) | off-chain aggregate | No — see `megapot-data-api` |
| Cumulative prizes paid | protocol | Data API (sum of `JackpotSettled.userWinnings` — `GET /v1/wallets/{address}/wins` per wallet, or `GET /v1/rounds/{id}/wins` per round) | off-chain aggregate | No — see `megapot-data-api` |

> **`lpEarnings` is gross, not net.** A common mistake is to label `getDrawingState(id).lpEarnings` (or `JackpotSettled.lpEarnings`) as "LP earnings paid out." That field holds the gross ticket revenue routed to the LP side — *before* winners are paid from the prize pool and *before* the protocol fee is taken. Use the "Net LP yield" formula above (or the LP pool delta) for the actual amount LPs took home for the drawing.

### Worked example — net LP yield for a settled drawing

For drawing `N`, the realized LP P&L is the gross revenue minus winners' payouts minus the protocol's cut:

```
realized LP P&L (drawing N)
   = lpEarnings              // gross revenue routed to LP side
   − drawingUserWinnings     // payouts from prize pool
   − protocolFeeAmount       // protocol cut of LP profit above threshold
```

Pull the three values from events for the same `drawingId`:

```ts
import { parseAbi } from 'viem';

const lpYieldEventsAbi = parseAbi([
  "event JackpotSettled(uint256 indexed drawingId, uint256 lpEarnings, uint256 userWinnings, uint8 winningBonusball, uint256 winningNumbers, uint256 newDrawingAccumulator)",
  "event ProtocolFeeCollected(uint256 indexed drawingId, uint256 amount)",
]);

const [settled, protocolFee] = await Promise.all([
  publicClient.getContractEvents({
    address: JACKPOT_ADDRESS,
    abi: lpYieldEventsAbi,
    eventName: 'JackpotSettled',
    args: { drawingId: pastDrawingId },
    fromBlock: 'earliest',
  }),
  publicClient.getContractEvents({
    address: JACKPOT_ADDRESS,
    abi: lpYieldEventsAbi,
    eventName: 'ProtocolFeeCollected',
    args: { drawingId: pastDrawingId },
    fromBlock: 'earliest',
  }),
]);

const grossLpRevenue   = settled[0].args.lpEarnings;
const userWinnings     = settled[0].args.userWinnings;
const protocolFeeAmt   = protocolFee[0]?.args.amount ?? 0n; // 0n if below protocolFeeThreshold
const netLpYield       = grossLpRevenue - userWinnings - protocolFeeAmt;
```

Display `grossLpRevenue` and `netLpYield` separately — labelling the gross value as "LP earnings paid out" is the documented anti-pattern this section exists to prevent.

### Worked example — winners per tier

Read the `WinnersCalculated` event for one settled drawing using `publicClient.getContractEvents` with a `drawingId` filter:

```ts
import { parseAbi } from 'viem';

const calculatorEventsAbi = parseAbi([
  "event WinnersCalculated(uint256 indexed drawingId, uint256[] winningNormals, uint256 winningBonusball, uint256[] uniqueResult, uint256[] dupResult)",
]);

const events = await publicClient.getContractEvents({
  address: JACKPOT_ADDRESS,
  abi: calculatorEventsAbi,
  eventName: 'WinnersCalculated',
  args: { drawingId: pastDrawingId },
  fromBlock: 'earliest', // or a known anchor block for the deployment
});

const { uniqueResult, dupResult } = events[0].args;
// uniqueResult[i] is the count of winners at tier i (no duplicates)
// dupResult[i]    is the count of winners at tier i that are duplicate combos
// total winners at tier i = uniqueResult[i] + dupResult[i]
```

Pair with `getExpectedDrawingTierPayouts(...)` to derive total payout per tier:

```ts
const tierPayouts = await publicClient.readContract({
  address: PAYOUT_CALC_ADDRESS,
  abi: payoutCalcAbi,
  functionName: 'getExpectedDrawingTierPayouts',
  args: [pastDrawingId, prizePool, ballMax, bonusballMax],
});

const totalsPerTier = uniqueResult.map((u, i) => (u + dupResult[i]) * tierPayouts[i]);
```

### Worked example — settlement latency

Compute the block delta between `JackpotLocked(drawingId)` and `JackpotSettled(drawingId)` for one drawing:

```ts
import { parseAbi } from 'viem';

const lifecycleEventsAbi = parseAbi([
  "event JackpotLocked(uint256 indexed drawingId)",
  "event JackpotSettled(uint256 indexed drawingId, uint256 lpEarnings, uint256 userWinnings, uint8 winningBonusball, uint256 winningNumbers, uint256 newDrawingAccumulator)",
]);

const [lockedEvents, settledEvents] = await Promise.all([
  publicClient.getContractEvents({
    address: JACKPOT_ADDRESS,
    abi: lifecycleEventsAbi,
    eventName: 'JackpotLocked',
    args: { drawingId: pastDrawingId },
    fromBlock: 'earliest',
  }),
  publicClient.getContractEvents({
    address: JACKPOT_ADDRESS,
    abi: lifecycleEventsAbi,
    eventName: 'JackpotSettled',
    args: { drawingId: pastDrawingId },
    fromBlock: 'earliest',
  }),
]);

const blocksToSettle = settledEvents[0].blockNumber - lockedEvents[0].blockNumber;
```

Average across the last N drawings to derive a "typical settlement time" homepage stat.

### Off-chain aggregates

For stats requiring off-chain aggregation (Cumulative ticket volume, Cumulative prizes paid, lifetime wallet aggregates), use the **Megapot Data API** at `api.megapot.io/v1`:

- `GET /v1/rounds` — paginated round history
- `GET /v1/wallets/{address}/stats` — wallet lifetime aggregate
- `GET /v1/rounds/{id}/wins` — per-round leaderboards

Direct RPC scans across the full drawing history are not viable for production homepages, and a self-hosted indexer is no longer required for these stats. See `megapot-data-api` ([/data-api](https://llms.megapot.io/data-api)) for the full skill.

## Reading Past-Drawing Match Status

Render "show me my tickets from past drawing N with match status (matched normals, matched bonusball, payout tier, claimable amount)" on a Tickets page.

> **Off-chain alternative:** prefer `GET /v1/wallets/{address}/tickets/rounds/{roundId}` (single past drawing) or `GET /v1/wallets/{address}/wins` (across-all-drawings) from the Data API — `matched_normals`, `bonusball_match`, and `winnings_amount` are pre-computed server-side, no per-drawing 5-call pipeline. The single-round winning numbers + per-tier payouts are also on `GET /v1/rounds/{roundId}` as `winning_numbers` and `prize_tiers`. See `megapot-data-api`. The on-chain recipe below stays the right path for keeper/agent flows that need fresh state without a Data API roundtrip, or when integrating directly with the contract from a non-React surface.

### Recipe

The full pipeline is 5 view calls plus client-side comparison. Most of the ABI fragments declared in "ABI Fragments" above (`jackpotAbi`, `payoutCalcAbi`, `ticketNftAbi`) are reused. One additional Jackpot function — `getUnpackedTicket` — is introduced here.

```ts
import { parseAbi } from 'viem';

const unpackAbi = parseAbi([
  "function getUnpackedTicket(uint256 _drawingId, uint256 _packedTicket) view returns (uint8[] normals, uint8 bonusball)",
]);

// PLACEHOLDER: drawing you want to inspect
const PAST_DRAWING_ID = currentDrawingId - 5n;

// Step 1 — read past drawing state (provides winningTicket, ballMax, bonusballMax, prizePool)
const pastDrawingState = await publicClient.readContract({
  address: JACKPOT_ADDRESS,
  abi: jackpotAbi,
  functionName: 'getDrawingState',
  args: [PAST_DRAWING_ID],
});
const { winningTicket, ballMax, bonusballMax, prizePool } = pastDrawingState;
```

Once `pastDrawingState` is known, batch the remaining 4 reads:

```ts
// Step 2 — read user tickets for that past drawing (returns already-unpacked normals/bonusball)
const userTickets = await publicClient.readContract({
  address: TICKET_NFT_ADDRESS,
  abi: ticketNftAbi,
  functionName: 'getUserTickets',
  args: [USER_ADDRESS, PAST_DRAWING_ID],
});

const ticketIds = userTickets.map((t) => t.ticketId);

// Steps 3–5 — batch unpack, tier IDs, and per-tier payouts
const [winningNumbers, tierIds, tierPayouts] = await publicClient.multicall({
  allowFailure: false,
  contracts: [
    {
      address: JACKPOT_ADDRESS,
      abi: unpackAbi,
      functionName: 'getUnpackedTicket',
      args: [PAST_DRAWING_ID, winningTicket],
    },
    {
      address: JACKPOT_ADDRESS,
      abi: jackpotAbi,
      functionName: 'getTicketTierIds',
      args: [ticketIds],
    },
    {
      address: PAYOUT_CALC_ADDRESS,
      abi: payoutCalcAbi,
      functionName: 'getExpectedDrawingTierPayouts',
      args: [PAST_DRAWING_ID, prizePool, ballMax, bonusballMax],
    },
  ],
});
```

> **Note on `getUnpackedTicket`:** this is the canonical contract-side decoder for packed `winningTicket` values. Do **not** reimplement the packing math in JavaScript — `getUnpackedTicket` is a `view` function and cheap to call.

### Step 6 — Compute match details

```ts
const winningNormalsSet = new Set(winningNumbers.normals);
const winningBonusball  = winningNumbers.bonusball;

const results = userTickets.map((ticket, i) => {
  const matchedNormals = ticket.normals.filter((n) => winningNormalsSet.has(n)).length;
  const bonusballMatched = ticket.bonusball === winningBonusball;
  const tierId = Number(tierIds[i]); // tierId = matchedNormals * 2 + (bonusballMatched ? 1 : 0)
  const claimableUsdc = tierPayouts[tierId];
  return {
    ticketId: ticket.ticketId,
    normals: ticket.normals,
    bonusball: ticket.bonusball,
    matchedNormals,
    bonusballMatched,
    tierId,
    claimableUsdc, // 6-decimal USDC; 0n for tier 0 / tier 2 (losing tiers)
  };
});

const totalClaimable = results.reduce((sum, r) => sum + r.claimableUsdc, 0n);
```

### Worked example — drawing 41

A user with 3 tickets in past drawing 41 where the winning numbers were `[2, 11, 17, 25, 28] / 19`:

```
Ticket 1001: [1, 9, 15, 23, 30] / 3   → 0 matches, no bonus → tier 0 → 0 USDC
Ticket 1042: [3, 11, 14, 22, 27] / 7  → 1 match (11), no bonus → tier 2 → 0 USDC
Ticket 1099: [2, 11, 18, 22, 28] / 19 → 3 matches (2, 11, 28) + bonus → tier 7 → tierPayouts[7] USDC
```

The tier formula `tierId = matchedNormals * 2 + (bonusballMatched ? 1 : 0)` is documented in [Prize Tier System](#prize-tier-system) above. Indices 0 and 2 of `tierPayouts` are always `0n` because those tiers do not pay out.

### Claim status

`getTicketTierIds` returns the tier even after the ticket has been claimed (the tier itself doesn't change). To detect "already claimed" status: a successful claim burns the ticket NFT, so check ownership via `JackpotTicketNFT.ownerOf(ticketId)`. If it reverts (or no longer matches `USER_ADDRESS`), the ticket was either claimed or transferred. See `megapot-claim-winnings` for the full claim flow.

## Related

- `megapot-data-api` — off-chain alternative for cross-drawing aggregates, wallet history, per-round outcomes (winning numbers + tier payouts on `GET /v1/rounds/{id}`)
- `megapot-buy-tickets` — buy 1–10 tickets with custom numbers
- `megapot-buy-random` — buy up to 10 random tickets (no number selection needed)
- `megapot-buy-bulk` — buy 11+ tickets with custom numbers (batch)
- `megapot-contracts-reference` — full address table and complete ABI for all 13 contracts
- `megapot-subscribe` — recurring automatic ticket purchases via `JackpotAutoSubscription`
- `megapot-lp-deposit` — deposit USDC into the LP pool
- `megapot-lp-withdraw` — initiate and finalize LP withdrawal
- `megapot-claim-winnings` — claim prize winnings after drawing settlement
- `megapot-claim-referral-fees` — claim accrued referral fees
