---
name: megapot-buy-tickets
description: Buy up to 10 Megapot lottery tickets on Base via Jackpot.buyTickets. Use for individual purchases. For bulk (more than 10), use megapot-buy-bulk instead.
---

# Buy Megapot Tickets (1–10)

## What This Does

Approves USDC spend and calls `Jackpot.buyTickets` to purchase up to 10 lottery tickets in a single transaction, returning an array of on-chain ticket IDs.

> ⚠️ Each ticket must specify 5 unique normals and a non-zero bonusball — `Jackpot.buyTickets` has no on-chain quick-pick. For random numbers, use [`megapot-buy-random`](../buy-random/SKILL.md) (1–10 immediate) or [`megapot-buy-bulk`](../buy-bulk/SKILL.md) (11+ via keeper). The contract's revert errors carry no message string, so a wrong ticket shape surfaces in viem/wagmi as `Execution reverted for an unknown reason` — pass the Jackpot ABI into `simulateContract` / `writeContract` so the selector decodes.

## Prerequisites

- A wallet with a private key (EOA) connected to Base mainnet (chain ID 8453)
- Sufficient USDC balance: `ticketPrice × numberOfTickets` (6 decimals)
- Sufficient ETH for gas
- `viem` installed (`npm install viem`)

## Addresses

Base mainnet (chain ID 8453):

```ts
const JACKPOT_ADDRESS  = '0x3bAe643002069dBCbcd62B1A4eb4C4A397d042a2' as const;
const USDC_ADDRESS     = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' as const;
```

Base Sepolia testnet (chain ID 84532):

```ts
const JACKPOT_ADDRESS  = '0x465dA3c859f193A3807386387bEE941B2A4c3279' as const;
const USDC_ADDRESS     = '0x036CbD53842c5426634e7929541eC2318f3dCF7e' 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

Only the fragments needed for this task:

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

const abi = parseAbi([
  // --- Jackpot reads ---
  "function ticketPrice() view returns (uint256)",
  "function normalBallMax() view returns (uint8)",
  "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))",

  // --- Jackpot write ---
  "function buyTickets((uint8[] normals, uint8 bonusball)[] _tickets, address _recipient, address[] _referrers, uint256[] _referralSplit, bytes32 _source) returns (uint256[] ticketIds)",

  // --- Jackpot events ---
  "event TicketPurchased(address indexed recipient, uint256 indexed currentDrawingId, bytes32 indexed source, uint256 userTicketId, uint8[] normals, uint8 bonusball, bytes32 referralScheme)",
  "event TicketOrderProcessed(address indexed buyer, address indexed recipient, uint256 indexed currentDrawingId, uint256 numberOfTickets, uint256 lpEarnings, uint256 referralFees)",

  // --- Jackpot errors ---
  "error InvalidBonusball()",
  "error InvalidNormalsCount()",
  "error InvalidTicketCount()",
  "error NoTicketsProvided()",
  "error TicketPurchasesDisabled()",
  "error TooManyReferrers()",
  "error JackpotLocked()",
  "error EmergencyEnabled()",

  // --- USDC ---
  "function balanceOf(address account) view returns (uint256)",
  "function allowance(address owner, address spender) view returns (uint256)",
  "function approve(address spender, uint256 amount) returns (bool)",
]);
```

## Recipe

### Setup

```ts
import {
  createPublicClient,
  createWalletClient,
  http,
  parseAbi,
  encodeFunctionData,
  zeroAddress,
} 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 USDC_ADDRESS    = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' as const;

// Your referrer address — you earn a percentage of every ticket sale and user winnings.
// Replace with your wallet address before deploying.
const REFERRER_ADDRESS = '0x0000000000000000000000000000000000000001' as const; // placeholder — replace with your address

// 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 abi = parseAbi([
  "function ticketPrice() view returns (uint256)",
  "function normalBallMax() view returns (uint8)",
  "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 buyTickets((uint8[] normals, uint8 bonusball)[] _tickets, address _recipient, address[] _referrers, uint256[] _referralSplit, bytes32 _source) returns (uint256[] ticketIds)",
  "event TicketPurchased(address indexed recipient, uint256 indexed currentDrawingId, bytes32 indexed source, uint256 userTicketId, uint8[] normals, uint8 bonusball, bytes32 referralScheme)",
  "event TicketOrderProcessed(address indexed buyer, address indexed recipient, uint256 indexed currentDrawingId, uint256 numberOfTickets, uint256 lpEarnings, uint256 referralFees)",
  "error InvalidBonusball()",
  "error InvalidNormalsCount()",
  "error InvalidTicketCount()",
  "error NoTicketsProvided()",
  "error TicketPurchasesDisabled()",
  "error TooManyReferrers()",
  "error JackpotLocked()",
  "error EmergencyEnabled()",
  "function balanceOf(address account) view returns (uint256)",
  "function allowance(address owner, address spender) view returns (uint256)",
  "function approve(address spender, uint256 amount) returns (bool)",
]);

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 — Read ticket price

```ts
const ticketPrice = await publicClient.readContract({
  address: JACKPOT_ADDRESS,
  abi,
  functionName: 'ticketPrice',
});
// e.g. 1_000_000n — 1 USDC at 6 decimals

const [normalBallMax, currentDrawingId] = await Promise.all([
  publicClient.readContract({ address: JACKPOT_ADDRESS, abi, functionName: 'normalBallMax' }),
  publicClient.readContract({ address: JACKPOT_ADDRESS, abi, functionName: 'currentDrawingId' }),
]);

const drawingState = await publicClient.readContract({
  address: JACKPOT_ADDRESS,
  abi,
  functionName: 'getDrawingState',
  args: [currentDrawingId],
});
const { bonusballMax } = drawingState;
// Validate your picks against: normals in [1, normalBallMax], bonusball in [1, bonusballMax]
```

### Step 2 — Approve USDC

```ts
const ticketCount = 3n;
const totalCost   = ticketPrice * ticketCount; // e.g. 3_000_000n for 3 × 1 USDC

const currentAllowance = await publicClient.readContract({
  address: USDC_ADDRESS,
  abi,
  functionName: 'allowance',
  args: [account.address, JACKPOT_ADDRESS],
});

if (currentAllowance < totalCost) {
  const approveTx = await walletClient.writeContract({
    address: USDC_ADDRESS,
    abi,
    functionName: 'approve',
    args: [JACKPOT_ADDRESS, totalCost],
  });
  await publicClient.waitForTransactionReceipt({ hash: approveTx });
  console.log('USDC approved:', approveTx);
}
```

### Step 3a — Buy 3 tickets with chosen numbers

```ts
// Concrete example: 3 tickets, each with 5 normals in [1, 30] ascending and bonusball in valid range
// Verify picks are valid: all normals in [1, normalBallMax], bonusball in [1, bonusballMax]
const tickets = [
  { normals: [3, 11, 14, 22, 27], bonusball: 7  },
  { normals: [5, 12, 18, 22, 29], bonusball: 12 },
  { normals: [1,  9, 15, 23, 30], bonusball: 3  },
];

const buyTx = await walletClient.writeContract({
  address: JACKPOT_ADDRESS,
  abi,
  functionName: 'buyTickets',
  args: [
    tickets,
    account.address,                          // _recipient  — who receives the NFT tickets
    [REFERRER_ADDRESS],                       // _referrers — your revenue address
    [1000000000000000000n],                   // _referralSplit — 100% to single referrer (1e18 scale)
    '0x0000000000000000000000000000000000000000000000000000000000000000' as `0x${string}`, // _source
  ],
});

const receipt = await publicClient.waitForTransactionReceipt({ hash: buyTx });
console.log('Tickets purchased, tx:', buyTx);
```

### Step 3b — Need random numbers instead?

`Jackpot.buyTickets` has no on-chain quick-pick — every entry in `_tickets` must be fully specified (5 normals + bonusball). For random tickets, use [`megapot-buy-random`](../buy-random/SKILL.md), which wraps `JackpotRandomTicketBuyer.buyTickets(count, …)` for 1–10 immediate random fills, or [`megapot-buy-bulk`](../buy-bulk/SKILL.md) for 11+ via the batch facilitator.

## Parameters

| Parameter | Type | Description |
|---|---|---|
| `_tickets` | `{ normals: uint8[], bonusball: uint8 }[]` | Array of ticket picks. 1–10 entries for this skill. Every entry must be fully specified — no quick-pick. For random tickets use [`megapot-buy-random`](../buy-random/SKILL.md). |
| `_tickets[].normals` | `uint8[]` | Normal ball selections. Must be exactly 5 unique values in ascending order, each in `[1, normalBallMax]` (typically `[1, 30]`). |
| `_tickets[].bonusball` | `uint8` | Bonus ball selection. Must be non-zero, in `[1, drawingState.bonusballMax]` (read `getDrawingState(currentDrawingId).bonusballMax`). |
| `_recipient` | `address` | Wallet that receives the ticket NFTs. Usually `account.address`. |
| `_referrers` | `address[]` | Your revenue address(es). You earn a share of ticket price and user winnings for every purchase. Up to `maxReferrers` addresses (typically 5). Always include at least your own address. |
| `_referralSplit` | `uint256[]` | Referral fee split weights per referrer in **1e18 (PRECISE_UNIT) scale, not basis points**. Must be same length as `_referrers` and sum to exactly `1000000000000000000` (`10n ** 18n`). Pass `[]` when no referrers. A single referrer getting 100% = `[1000000000000000000n]` (10^18). A 70/30 split = `[700000000000000000n, 300000000000000000n]`. |
| `_source` | `bytes32` | Integration source tag for on-chain analytics. For production integrations, use a unique identifier for your app (e.g. `keccak256('your-app-name')` cast to `bytes32`) — this enables on-chain attribution and analytics. Use `0x000...0` for testing. |

> To purchase without referral attribution (no fees earned), pass empty arrays: `_referrers: []`, `_referralSplit: []`.

### Valid ranges (read dynamically from contract)

| Range | How to read | Typical testnet value |
|---|---|---|
| Normal ball: `[1, ballMax]` | `normalBallMax()` (protocol config, equals drawing's `ballMax`) | 30 |
| Bonus ball: `[1, bonusballMax]` | `getDrawingState(currentDrawingId).bonusballMax` | 30 |
| Ticket price (USDC, 6 dec) | `ticketPrice()` | 1_000_000 |

> **Note:** `bonusballMin()`, `bonusballSoftCap()`, and `bonusballHardCap()` are protocol configuration parameters used internally to determine what `bonusballMax` gets set to for each drawing. They are **not** the user-facing pick range. The bonusball pick range is always `[1, drawingState.bonusballMax]`.

## Common Errors

| Error | Cause |
|---|---|
| `InvalidBonusball()` | `bonusball` is 0 or greater than `drawingState.bonusballMax` |
| `InvalidNormalsCount()` | `normals.length` is not exactly 5 |
| `InvalidTicketCount()` | Ticket array is empty or exceeds the maximum allowed (use `megapot-buy-bulk` for more than 10) |
| `NoTicketsProvided()` | `_tickets` array is empty |
| `TicketPurchasesDisabled()` | Contract owner has temporarily disabled purchases |
| `TooManyReferrers()` | `_referrers` length exceeds `maxReferrers()` |
| `JackpotLocked()` | Drawing is in the lock period immediately before a draw; try again after settlement |
| `EmergencyEnabled()` | Contract is in emergency mode; purchases are halted |
| `SafeERC20FailedOperation` | USDC `approve` or `transferFrom` failed — check balance and allowance |

## Related

- `megapot-buy-random` — buy up to 10 random tickets (no number selection needed)
- `megapot-buy-bulk` — for purchasing more than 10 tickets with custom numbers via `BatchPurchaseFacilitator`
- `megapot-contracts-reference` — full address table and complete ABI for all 13 contracts
- `megapot-subscribe` — for recurring automatic ticket purchases via `JackpotAutoSubscription`
- `megapot-claim-winnings` — for claiming prizes after a drawing settles
- `megapot-claim-referral-fees` — for collecting accrued referral fees
