---
name: megapot-claim-winnings
description: Find unclaimed winning Megapot tickets and claim their USDC payouts via Jackpot.claimWinnings. Use after drawings settle.
---

# Claim Megapot Winnings

## What This Does

Scans the last 10 completed drawings for your unclaimed winning tickets, then calls `Jackpot.claimWinnings` to transfer USDC prizes to your wallet. Tickets are chunked into groups of 50 per call for gas safety.

## Prerequisites

- A wallet with a private key (EOA) connected to Base mainnet (chain ID 8453)
- One or more Megapot ticket NFTs that have been through a completed drawing
- Sufficient ETH for gas
- `viem` installed (`npm install viem`)

## Addresses

Base mainnet (chain ID 8453):

```ts
const JACKPOT_ADDRESS     = '0x3bAe643002069dBCbcd62B1A4eb4C4A397d042a2' as const;
const TICKET_NFT_ADDRESS  = '0x48FfE35AbB9f4780a4f1775C2Ce1c46185b366e4' as const;
```

Base Sepolia testnet (chain ID 84532):

```ts
const JACKPOT_ADDRESS     = '0x465dA3c859f193A3807386387bEE941B2A4c3279' as const;
const TICKET_NFT_ADDRESS  = '0x45084829ac63f9dC6a3D4981A46FA896f9180ECd' 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).

## Winning-Check Mechanism

There is no `isWinningTicket` function. Instead use:

```
Jackpot.getTicketTierIds(uint256[] _ticketIds) view returns (uint256[] tierIds)
```

Each position in the returned array corresponds to the same position in `_ticketIds`. The tier ID encodes how many numbers matched:

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

**Important:** Tiers `0` (no match) and `2` (1 normal match, no bonusball) do NOT pay out. A ticket is a winner when `tierId > 0 && tierId !== 2n`. Filter for this condition to find claimable tickets.

**Unsettled drawings:** `getTicketTierIds` returns `0` for all tickets in a drawing that has not yet been settled (i.e., where `getDrawingState(drawingId).winningTicket === 0`). Always verify the drawing is settled before interpreting tier results. The current active drawing (`currentDrawingId()`) is typically unsettled — check the previous drawing (`currentDrawingId - 1n`) for the most recent results.

## ABI

Only the fragments needed for this task:

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

const jackpotAbi = parseAbi([
  // --- reads ---
  "function currentDrawingId() view returns (uint256)",
  "function getTicketTierIds(uint256[] _ticketIds) view returns (uint256[] tierIds)",

  // --- write ---
  "function claimWinnings(uint256[] _userTicketIds)",

  // --- events ---
  "event TicketWinningsClaimed(address indexed userAddress, uint256 indexed drawingId, uint256 userTicketId, uint256 matchedNormals, bool bonusballMatch, uint256 winningsAmount)",

  // --- errors ---
  "error NoTicketsToClaim()",
  "error NotTicketOwner()",
  "error InvalidDrawingId()",
]);

const nftAbi = parseAbi([
  // getUserTickets returns full ExtendedTicketInfo structs for one drawing
  "function getUserTickets(address _userAddress, uint256 _drawingId) view returns ((uint256 ticketId, (uint256 drawingId, uint256 packedTicket, bytes32 referralScheme) ticket, uint8[] normals, uint8 bonusball)[])",
]);

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

const jackpotReadAbi = parseAbi([
  "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))",
]);
```

## Recipe

### Setup

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

// --- PLACEHOLDERS: replace with your values ---
const PRIVATE_KEY = '0xYOUR_PRIVATE_KEY' as `0x${string}`;  // placeholder
const RPC_URL     = 'https://mainnet.base.org';               // or your own RPC URL

const JACKPOT_ADDRESS    = '0x3bAe643002069dBCbcd62B1A4eb4C4A397d042a2' as const;
const TICKET_NFT_ADDRESS = '0x48FfE35AbB9f4780a4f1775C2Ce1c46185b366e4' as const;

// 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.

const jackpotAbi = parseAbi([
  "function currentDrawingId() view returns (uint256)",
  "function getTicketTierIds(uint256[] _ticketIds) view returns (uint256[] tierIds)",
  "function claimWinnings(uint256[] _userTicketIds)",
  "event TicketWinningsClaimed(address indexed userAddress, uint256 indexed drawingId, uint256 userTicketId, uint256 matchedNormals, bool bonusballMatch, uint256 winningsAmount)",
  "error NoTicketsToClaim()",
  "error NotTicketOwner()",
  "error InvalidDrawingId()",
]);

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

const account = privateKeyToAccount(PRIVATE_KEY);

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

const walletClient = createWalletClient({
  account,
  chain: base,
  transport: http(RPC_URL),
});
```

### Step 1 — Get currentDrawingId

```ts
const currentDrawingId = await publicClient.readContract({
  address: JACKPOT_ADDRESS,
  abi: jackpotAbi,
  functionName: 'currentDrawingId',
});
// e.g. 42n — the active (unsettled) drawing

// Walk back 10 completed drawings. Drawing IDs are 1-indexed, so settled
// drawings are currentDrawingId-1 down to max(1, currentDrawingId-10).
const LOOK_BACK = 10n;
const firstId   = currentDrawingId > LOOK_BACK ? currentDrawingId - LOOK_BACK : 1n;

const drawingIds: bigint[] = [];
for (let id = firstId; id < currentDrawingId; id++) {
  drawingIds.push(id);
}
// drawingIds = [32n, 33n, ..., 41n]  (up to 10 entries)
```

> **Off-chain alternative.** For "show me unclaimed wins" feeds (the common UI case), the entire Step 2 + Step 3 RPC scan below collapses to one call. Pass `?claimed=false` (added in API v1.6.0) so already-claimed IDs are filtered server-side — `claimWinnings` reverts with `NoTicketsToClaim()` if every supplied ID has been claimed:
>
> ```ts
> const wins = await api.walletWins(address, { claimed: false }); // GET /v1/wallets/{address}/wins?claimed=false
> const ticketIds = wins.data.map(w => BigInt(w.user_ticket_id)); // user_ticket_id is a stringified uint256
> await walletClient.writeContract({
>   address: JACKPOT_ADDRESS,
>   abi: jackpotAbi,
>   functionName: 'claimWinnings',
>   args: [ticketIds],
> });
> ```
>
> The Data API returns wins across every past drawing with `claimed`, `amount`, and `user_ticket_id` already attached — no per-drawing scan, no `getTicketTierIds` chunking. The `?claimed=false` filter matters especially for smart-contract-account claim flows (Bankr, Safe, etc.) where re-submitting claimed IDs has a real economic cost. The `BigInt()` cast is required at the API → contract handoff (see `megapot-data-api` § "Helpers / Shape Parity").
>
> Keep the per-drawing RPC pipeline below when you need fresh state without a Data API roundtrip — keepers, agents, non-React surfaces, or any flow where you can't accept up to one block of indexer lag.

### Step 2 — Collect your tickets for each drawing

```ts
// getUserTickets returns an empty array when you hold no tickets for a drawing,
// so it is safe to call for every ID unconditionally.
const perDrawingResults = await Promise.all(
  drawingIds.map((drawingId) =>
    publicClient.readContract({
      address: TICKET_NFT_ADDRESS,
      abi: nftAbi,
      functionName: 'getUserTickets',
      args: [account.address, drawingId],
    })
  )
);

// Flatten to a single list of ticket IDs (bigint)
const allTicketIds: bigint[] = perDrawingResults
  .flat()
  .map((info) => info.ticketId);

console.log(`Found ${allTicketIds.length} tickets across ${drawingIds.length} drawings`);
// e.g. "Found 17 tickets across 10 drawings"

if (allTicketIds.length === 0) {
  console.log('No tickets found — nothing to check.');
  process.exit(0);
}
```

### Step 3 — Check each ticket for winnings via getTicketTierIds

```ts
// getTicketTierIds can be called with the full array in one RPC call,
// but chunk to 200 to avoid hitting node response-size limits.
const TIER_CHECK_CHUNK = 200;

async function chunkTierIds(ids: bigint[]): Promise<bigint[]> {
  const results: bigint[] = [];
  for (let i = 0; i < ids.length; i += TIER_CHECK_CHUNK) {
    const slice = ids.slice(i, i + TIER_CHECK_CHUNK);
    const tiers = await publicClient.readContract({
      address: JACKPOT_ADDRESS,
      abi: jackpotAbi,
      functionName: 'getTicketTierIds',
      args: [slice],
    });
    results.push(...tiers);
  }
  return results;
}

const tierIds = await chunkTierIds(allTicketIds);

// tierIds[i] === 0n  → no match (not a winner)
// tierIds[i] === 2n  → 1 normal match, no bonusball (not a winner, no payout)
// all other values   → winner (has a USDC payout)
const winningTicketIds: bigint[] = allTicketIds.filter(
  (_, i) => tierIds[i] > 0n && tierIds[i] !== 2n
);

console.log(`Winning tickets: ${winningTicketIds.length} of ${allTicketIds.length}`);
// e.g. "Winning tickets: 3 of 17"

if (winningTicketIds.length === 0) {
  console.log('No winning tickets found.');
  process.exit(0);
}
```

### Step 4 — Claim in chunks of 50

```ts
const CLAIM_CHUNK_SIZE = 50;

function chunk<T>(arr: T[], size: number): T[][] {
  const chunks: T[][] = [];
  for (let i = 0; i < arr.length; i += size) {
    chunks.push(arr.slice(i, i + size));
  }
  return chunks;
}

const ticketChunks = chunk(winningTicketIds, CLAIM_CHUNK_SIZE);
console.log(`Submitting ${ticketChunks.length} claim transaction(s)…`);

for (let i = 0; i < ticketChunks.length; i++) {
  const ids = ticketChunks[i];
  console.log(`  Chunk ${i + 1}/${ticketChunks.length}: ${ids.length} ticket(s)`);

  const claimTx = await walletClient.writeContract({
    address: JACKPOT_ADDRESS,
    abi: jackpotAbi,
    functionName: 'claimWinnings',
    args: [ids],
  });

  const receipt = await publicClient.waitForTransactionReceipt({ hash: claimTx });
  console.log(`  Claimed. tx: ${claimTx}  status: ${receipt.status}`);
}

console.log('All winnings claimed.');
```

### Optional — Decode TicketWinningsClaimed events

After each receipt, you can parse payout details from logs:

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

const logs = parseEventLogs({
  abi: jackpotAbi,
  eventName: 'TicketWinningsClaimed',
  logs: receipt.logs,
});

for (const log of logs) {
  const { userTicketId, matchedNormals, bonusballMatch, winningsAmount } = log.args;
  console.log(
    `  Ticket #${userTicketId}: ${matchedNormals} normal(s) matched, ` +
    `bonusball=${bonusballMatch}, payout=${winningsAmount} USDC units` +
    ` (${Number(winningsAmount) / 1e6} USDC)`
  );
}
// e.g. "Ticket #1042: 4 normal(s) matched, bonusball=true, payout=50000000 USDC units (50 USDC)"
```

## Parameters

### `Jackpot.claimWinnings`

| Parameter | Type | Description |
|---|---|---|
| `_userTicketIds` | `uint256[]` | Array of ticket NFT IDs to claim. All must be owned by the caller. |

### `JackpotTicketNFT.getUserTickets`

| Parameter | Type | Description |
|---|---|---|
| `_userAddress` | `address` | Wallet to look up. |
| `_drawingId` | `uint256` | Drawing to query. Returns empty array if no tickets held. |

### `Jackpot.getTicketTierIds`

| Parameter | Type | Description |
|---|---|---|
| `_ticketIds` | `uint256[]` | Ticket IDs to check. Returns one `uint256` per ID. `tierId = normalMatches * 2 + bonusballMatch`. Tiers `0` and `2` have no payout; all other tier IDs are winners. |

## Common Errors

| Error | Cause |
|---|---|
| `NoTicketsToClaim()` | All supplied ticket IDs have already been claimed or have tier 0 (no win) |
| `NotTicketOwner()` | A ticket in the array is not owned by the calling wallet |
| `InvalidDrawingId()` | A ticket references a drawing ID that has not yet been initialized |

### Getting payout amounts

To display how much USDC each winning tier pays, use `GuaranteedMinimumPayoutCalculator.getExpectedDrawingTierPayouts` — see `megapot-read-state` for the full recipe. This requires reading `getDrawingState` first to get `prizePool`, `ballMax`, and `bonusballMax`.

```ts
// After getting drawingState from getDrawingState(drawingId):
const drawingState = await publicClient.readContract({
  address: JACKPOT_ADDRESS,
  abi: jackpotReadAbi,
  functionName: 'getDrawingState',
  args: [drawingId],
});

const tierPayouts = await publicClient.readContract({
  address: '0x97a22361b6208aC8cd9afaea09D20feC47046CBD', // GuaranteedMinimumPayoutCalculator (mainnet)
  abi: payoutCalcAbi,
  functionName: 'getExpectedDrawingTierPayouts',
  args: [drawingId, drawingState.prizePool, drawingState.ballMax, drawingState.bonusballMax],
});
// tierPayouts[tierId] = per-ticket USDC payout (6 decimals) for that tier
```

## Related

- `megapot-data-api` — `GET /v1/wallets/{address}/wins?claimed=false` for "show me unclaimed wins" UI feeds (read-only); pair with this skill for the write side
- `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 contracts
- `megapot-claim-referral-fees` — collect accrued referral fees via `Jackpot.claimReferralFees`
