---
name: megapot-buy-bulk
description: Bulk-purchase more than 10 Megapot lottery tickets via BatchPurchaseFacilitator — creates a keeper-executed batch order and polls for completion. For 10 or fewer tickets use megapot-buy-tickets.
---

# Buy Megapot Tickets in Bulk (11+)

## What This Does

Approves USDC spend to `BatchPurchaseFacilitator`, calls `createBatchOrder` to register a 10–N ticket order, then polls `getBatchOrderInfo` every 5 seconds until the keeper executes all tickets. Supports a mix of dynamic (quick-pick) and static (custom-number) tickets. Can also cancel an in-progress order with `cancelBatchOrder`.

Supports both custom-number tickets (`_userStaticTickets`) and random tickets (`_dynamicTicketCount`). **Cap `_userStaticTickets` at 10 entries per order.** The contract itself does not enforce a length, but Base's per-block calldata budget makes large static arrays risky to settle, and megapot.io's own UI caps static picks at 10 per order — following the same convention keeps your integration consistent. There is no recommended cap on dynamic tickets (those are generated keeper-side and don't add per-ticket calldata). To purchase more than 10 custom-number tickets, place multiple orders. For all-random purchases of 11+ tickets, pass an empty static tickets array — see the "Buying All-Random Tickets" section below.

**Key difference from `megapot-buy-tickets`:** The batch facilitator is keeper-executed, meaning execution happens across one or more subsequent transactions by an off-chain keeper. The order is fully funded up front; any unspent USDC is refunded automatically.

## Prerequisites

- A wallet with a private key (EOA) connected to Base mainnet (chain ID 8453)
- Sufficient USDC balance: `ticketPrice × totalTicketCount` (6 decimals)
- Sufficient ETH for gas
- Minimum ticket count is 10 (enforced by `minimumTicketCount()` on the contract)
- `viem` installed (`npm install viem`)

## Addresses

Base mainnet (chain ID 8453):

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

Base Sepolia testnet (chain ID 84532):

```ts
const BATCH_FACILITATOR_ADDRESS = '0xe582dD908Ca5bd51C743DFdda37C93bBaCD27c56' as const;
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)",

  // --- BatchPurchaseFacilitator reads ---
  "function minimumTicketCount() view returns (uint256)",
  "function hasActiveBatchOrder(address _recipient) view returns (bool)",
  "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))",

  // --- BatchPurchaseFacilitator writes ---
  "function createBatchOrder(address _recipient, uint64 _dynamicTicketCount, (uint8[] normals, uint8 bonusball)[] _userStaticTickets, address[] _referrers, uint256[] _referralSplit)",
  "function cancelBatchOrder()",

  // --- BatchPurchaseFacilitator events ---
  "event BatchOrderCreated(address indexed payer, address indexed recipient, uint256 indexed drawingId, uint256 totalCost, uint256 dynamicTicketCount, uint256 staticTicketCount)",
  "event BatchOrderExecuted(address indexed user, uint256 indexed drawingId, uint256[] ticketIds, uint256 ticketsExecuted, uint256 remainingTickets, uint256 remainingUSDC)",
  "event BatchOrderCancelled(address indexed recipient, uint8 indexed executionAction, uint256 refundAmount)",
  "event BatchOrderRemoved(address indexed recipient)",

  // --- BatchPurchaseFacilitator errors ---
  "error ActiveBatchOrderExists()",
  "error NoActiveBatchOrder()",
  "error InvalidTicketCount()",
  "error InvalidStaticTicket()",
  "error InvalidNormalBallCount()",
  "error JackpotLocked()",
  "error JackpotNotInitialized()",

  // --- 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,
} 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 BATCH_FACILITATOR_ADDRESS = '0x01774B531591b286b9f02C6Bc02ab3fD9526Aa76' as const;
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 minimumTicketCount() view returns (uint256)",
  "function hasActiveBatchOrder(address _recipient) view returns (bool)",
  "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))",
  "function createBatchOrder(address _recipient, uint64 _dynamicTicketCount, (uint8[] normals, uint8 bonusball)[] _userStaticTickets, address[] _referrers, uint256[] _referralSplit)",
  "function cancelBatchOrder()",
  "event BatchOrderCreated(address indexed payer, address indexed recipient, uint256 indexed drawingId, uint256 totalCost, uint256 dynamicTicketCount, uint256 staticTicketCount)",
  "event BatchOrderExecuted(address indexed user, uint256 indexed drawingId, uint256[] ticketIds, uint256 ticketsExecuted, uint256 remainingTickets, uint256 remainingUSDC)",
  "event BatchOrderCancelled(address indexed recipient, uint8 indexed executionAction, uint256 refundAmount)",
  "error ActiveBatchOrderExists()",
  "error NoActiveBatchOrder()",
  "error InvalidTicketCount()",
  "error InvalidStaticTicket()",
  "error InvalidNormalBallCount()",
  "error JackpotLocked()",
  "error JackpotNotInitialized()",
  "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 and minimum ticket count

```ts
const [ticketPrice, minimumTicketCount] = await Promise.all([
  publicClient.readContract({
    address: JACKPOT_ADDRESS,
    abi,
    functionName: 'ticketPrice',
  }),
  publicClient.readContract({
    address: BATCH_FACILITATOR_ADDRESS,
    abi,
    functionName: 'minimumTicketCount',
  }),
]);
// ticketPrice e.g. 1_000_000n — 1 USDC at 6 decimals
// minimumTicketCount e.g. 10n — must order at least this many
```

### Step 2 — Approve USDC to BatchPurchaseFacilitator (not Jackpot)

USDC approval must be granted to `BATCH_FACILITATOR_ADDRESS`, not the Jackpot contract.

```ts
// 50 total tickets: 40 dynamic + 10 static
// (static tickets are capped at 10 per order — see Parameters below)
const DYNAMIC_COUNT = 40n;
const STATIC_COUNT  = 10n;
const TOTAL_TICKETS = DYNAMIC_COUNT + STATIC_COUNT; // 50n

const totalCost = ticketPrice * TOTAL_TICKETS;
// e.g. 1_000_000n * 50n = 50_000_000n  (50 USDC at 6 decimals)

const currentAllowance = await publicClient.readContract({
  address: USDC_ADDRESS,
  abi,
  functionName: 'allowance',
  args: [account.address, BATCH_FACILITATOR_ADDRESS], // spender is the facilitator
});

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

### Step 3 — Create the batch order (40 dynamic + 10 static tickets)

`_dynamicTicketCount` is the number of quick-pick tickets the keeper will generate. The `_userStaticTickets` array supplies custom-number tickets — keep it at **≤10 entries per order** (recommended cap; see Parameters below). To purchase more than 10 custom tickets, place additional orders. Normal balls must be 5 unique values in ascending order, each in `[1, normalBallMax]` (typically `[1, 30]`); bonusball in `[1, drawingState.bonusballMax]` (read `getDrawingState(currentDrawingId).bonusballMax`).

```ts
// 10 static tickets with concrete pick sets (the recommended per-order cap)
// normals: exactly 5 unique values in [1, 30] ascending; bonusball in valid range
const staticTickets = [
  { normals: [3,  11, 14, 22, 27], bonusball: 7  },
  { normals: [5,  12, 18, 22, 29], bonusball: 12 },
  { normals: [1,   9, 15, 23, 30], bonusball: 3  },
  { normals: [2,  11, 17, 25, 28], bonusball: 19 },
  { normals: [7,  13, 16, 24, 29], bonusball: 5  },
  { normals: [4,  10, 13, 20, 26], bonusball: 28 },
  { normals: [6,  14, 17, 21, 28], bonusball: 9  },
  { normals: [8,  15, 19, 24, 30], bonusball: 15 },
  { normals: [2,  10, 16, 23, 27], bonusball: 2  },
  { normals: [3,  12, 18, 24, 29], bonusball: 20 },
] as const;

const createTx = await walletClient.writeContract({
  address: BATCH_FACILITATOR_ADDRESS,
  abi,
  functionName: 'createBatchOrder',
  args: [
    account.address,                          // _recipient — who receives the ticket NFTs
    40n,                                      // _dynamicTicketCount — 40 quick-pick tickets
    staticTickets,                            // _userStaticTickets — 10 custom-number tickets (recommended cap)
    [REFERRER_ADDRESS],                       // _referrers — your revenue address
    [1000000000000000000n],                   // _referralSplit — 100% to single referrer (1e18 scale)
  ],
});

const receipt = await publicClient.waitForTransactionReceipt({ hash: createTx });
console.log('Batch order created, tx:', createTx);
// The BatchOrderCreated event confirms: drawingId, totalCost, dynamicTicketCount, staticTicketCount
```

### Step 4 — Poll getBatchOrderInfo every 5 s for completion

The keeper executes the order in one or more batches. Poll until `remainingTickets` reaches 0.

> **wagmi tip:** `getBatchOrderInfo` reverts with `NoActiveBatchOrder` when no order exists. Set `query: { retry: false }` to avoid unnecessary retries. Use `hasActiveBatchOrder(address)` first for a non-reverting boolean check.

```ts
async function pollBatchOrder(recipient: `0x${string}`) {
  const POLL_INTERVAL_MS = 5_000; // 5 seconds

  console.log('Polling for batch order completion…');

  while (true) {
    const isActive = await publicClient.readContract({
      address: BATCH_FACILITATOR_ADDRESS,
      abi,
      functionName: 'hasActiveBatchOrder',
      args: [recipient],
    });

    if (!isActive) {
      console.log('Batch order completed (no active order found).');
      break;
    }

    const info = await publicClient.readContract({
      address: BATCH_FACILITATOR_ADDRESS,
      abi,
      functionName: 'getBatchOrderInfo',
      args: [recipient],
    });

    const { orderDrawingId, remainingUSDC, remainingTickets, totalTicketsOrdered } =
      info.batchOrder;

    const executed = totalTicketsOrdered - remainingTickets;
    console.log(
      `Drawing ${orderDrawingId} | executed: ${executed}/${totalTicketsOrdered} | ` +
      `remaining tickets: ${remainingTickets} | remaining USDC: ${remainingUSDC}`
    );

    if (remainingTickets === 0n) {
      console.log('All tickets executed. Order complete.');
      break;
    }

    await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
  }
}

await pollBatchOrder(account.address);
```

### Step 5 (optional) — Cancel an in-progress order

`cancelBatchOrder` is called by the **recipient's wallet** (not the payer). It refunds any unspent USDC.

```ts
// Only works if there is an active batch order for account.address
const hasOrder = await publicClient.readContract({
  address: BATCH_FACILITATOR_ADDRESS,
  abi,
  functionName: 'hasActiveBatchOrder',
  args: [account.address],
});

if (!hasOrder) {
  console.log('No active batch order to cancel.');
} else {
  const cancelTx = await walletClient.writeContract({
    address: BATCH_FACILITATOR_ADDRESS,
    abi,
    functionName: 'cancelBatchOrder',
    // No arguments — cancels the order for msg.sender (the recipient)
  });
  await publicClient.waitForTransactionReceipt({ hash: cancelTx });
  console.log('Batch order cancelled, tx:', cancelTx);
  // BatchOrderCancelled event emits: recipient, executionAction, refundAmount
}
```

## Buying All-Random Tickets (No Custom Numbers)

To buy 11+ random tickets without specifying any numbers, set `_dynamicTicketCount` to your desired total and pass an empty array `[]` for `_userStaticTickets`. The keeper generates all ticket numbers randomly.

### Example: Buy 50 random tickets

```ts
const createTx = await walletClient.writeContract({
  address: BATCH_FACILITATOR_ADDRESS,
  abi,
  functionName: 'createBatchOrder',
  args: [
    account.address,              // _recipient
    50n,                          // _dynamicTicketCount — all 50 are random
    [],                           // _userStaticTickets — EMPTY for all random
    [REFERRER_ADDRESS],           // _referrers
    [1000000000000000000n],       // _referralSplit — 100% to single referrer (1e18)
  ],
});
await publicClient.waitForTransactionReceipt({ hash: createTx });
console.log('Batch order created for 50 random tickets');

// Poll for completion using the same pattern from the main recipe above
```

This is equivalent to using `JackpotRandomTicketBuyer` but for quantities above 10. The trade-off is that batch orders are keeper-executed (not immediate) — expect a short delay (seconds to minutes) before tickets are minted.

## Parameters

| Parameter | Type | Description |
|---|---|---|
| `_recipient` | `address` | Wallet that receives the ticket NFTs. Usually `account.address`. |
| `_dynamicTicketCount` | `uint64` | Number of quick-pick (random number) tickets. Combined with static count must be >= `minimumTicketCount()`. |
| `_userStaticTickets` | `{ normals: uint8[], bonusball: uint8 }[]` | Array of custom-number ticket picks. **Recommended maximum: 10 entries per order.** The contract does not enforce a length cap, but two factors push you to keep this small: Base's per-block calldata budget (large static arrays risk failing or settling unpredictably) and parity with megapot.io's UI, which artificially caps static picks at 10 per order. To buy more than 10 custom tickets, place multiple orders. Each `normals` must be exactly 5 unique values in ascending order, each in `[1, normalBallMax]` (typically `[1, 30]`); `bonusball` in `[1, drawingState.bonusballMax]` (read `getDrawingState(currentDrawingId).bonusballMax`). |
| `_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]`. |

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

### getBatchOrderInfo return shape

```ts
{
  batchOrder: {
    orderDrawingId:     bigint,   // drawing this order belongs to
    remainingUSDC:      bigint,   // USDC not yet spent (6 decimals)
    remainingTickets:   bigint,   // tickets not yet executed
    totalTicketsOrdered: bigint,  // original total (dynamic + static)
    dynamicTicketCount: bigint,   // original dynamic count
    referrers:          string[], // referrer addresses
    referralSplit:      bigint[], // 1e18-scale weight per referrer
  },
  staticTickets: { normals: number[], bonusball: number }[],
}
```

## Common Errors

| Error | Cause |
|---|---|
| `ActiveBatchOrderExists()` | A batch order for this recipient already exists; wait for it to complete or cancel it first |
| `NoActiveBatchOrder()` | `cancelBatchOrder` called when there is no active order for `msg.sender` |
| `InvalidTicketCount()` | Total ticket count (dynamic + static) is below `minimumTicketCount()` |
| `InvalidStaticTicket()` | A static ticket has an out-of-range normal ball or bonusball value (per-ticket validation; this error does not signal an array-length problem — see `_userStaticTickets` parameter notes for the recommended 10-entry cap) |
| `InvalidNormalBallCount()` | `normals` array length does not equal 5 (the required count for this drawing) |
| `JackpotLocked()` | Drawing is in the lock period; try again after settlement |
| `JackpotNotInitialized()` | Jackpot contract has not been initialized yet |
| `SafeERC20FailedOperation` | USDC `approve` or `transferFrom` failed — check balance and that you approved `BATCH_FACILITATOR_ADDRESS`, not Jackpot |

## Related

- `megapot-buy-tickets` — buy 1–10 tickets with custom numbers via `Jackpot.buyTickets`
- `megapot-buy-random` — buy up to 10 random tickets (no number selection needed)
- `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
