---
name: cigpack
description: Mint, buy and sell CigPack NFTs (3,333 fully onchain pixel cig packs) on BNB Smart Chain. Mint is one contract call; trading uses the CigPack market contract.
chain_id: 56
contract: "0x5303C952dFE360D2c76f880447F2E0e3A7Fd0cF3"
market: "0xCf9cd094bEab73fb8d5096Ccb50b224A64f26043"
site: https://cigpacks.fun
x: https://x.com/Cig_Packs
---

# Mint CigPack as an agent

CigPack is an ERC-721 on BNB Smart Chain (chain id 56). There's no API, allowlist or signature from us. You need a wallet that can sign a transaction and a little BNB.

- Contract: `0x5303C952dFE360D2c76f880447F2E0e3A7Fd0cF3`
- Chain: BNB Smart Chain, chain id `56`, RPC e.g. `https://bsc-dataseed.binance.org`
- Supply: 3,333. Max 5 per wallet.
- Full ABI: https://cigpacks.fun/cigpack.abi.json

## Functions you need

```solidity
function mintOpen() view returns (bool)
function totalMinted() view returns (uint256)
function MAX_SUPPLY() view returns (uint256)              // 3333
function mintedBy(address wallet) view returns (uint256)  // 0..5
function quote(address wallet, uint256 qty) view returns (uint256)  // exact wei to send
function mint(uint256 qty) payable
// optional, for display
function hasAgentId(address wallet) view returns (bool)
function agentIdOf(address wallet) view returns (uint256)
function discountAvailable(address wallet) view returns (bool)
```

## Steps

1. Read `mintOpen()`. If false, stop and try later.
2. Read `mintedBy(W)`, where `W` is the address that will send the mint transaction. Pick `qty` from 1 to `5 - mintedBy(W)`. Also keep `totalMinted() + qty <= 3333`.
3. Call `quote(W, qty)`. The result is the exact price in wei.
4. Send `mint(qty)` from `W` with `value = quote(W, qty)`. It must be exact. More or less reverts with `WrongPayment(expected)`.
5. Wait for the receipt. Each new token emits `Transfer(0x0, W, tokenId)`.
6. Report the tx hash and token IDs to your human owner, then ask them: reveal the new pack(s) now, or keep them sealed? You MUST ask, and you may call `requestReveal` / `reveal` only after they say yes. See "After mint: reveal" below.

Always quote with the same address that sends the transaction, and quote right before sending. The price depends on the sender.

## Price rules (BNB)

| Mint # for this wallet | Wallet holds a .ai Agent ID | No .ai |
|---|---|---|
| 1 | free (send the 0.0001 anti-Sybil fee) | 0.0005 |
| 2 | 0.0005 | 0.001 |
| 3 | 0.001 | 0.0015 |
| 4 | 0.0015 | 0.002 |
| 5 | 0.002 | 0.0025 |

- .ai Agent ID = the SPACE ID Agent ID registry on BNB Chain, `0x60B9262456dE011F7d15a54CdD8c48bAaa8B022F`. The contract finds the wallet's ID via `handleOf(wallet)`, using `tokenId = uint256(keccak256(bytes(handle)))`. It checks the result with `ownerOf`.
- The first mint is free for .ai holders, but you must still send a 0.0001 BNB anti-Sybil fee with it (it's already included in `quote`). Sending 0 reverts with `WrongPayment`.
- The free first mint is used up once per wallet and once per Agent ID. After that the wallet pays the normal ladder.
- A batch is priced as consecutive steps, e.g. a fresh .ai wallet minting 3 sends 0.0001 (fee for the free pack) + 0.0005 + 0.001 = 0.0016.
- Never hardcode the price. `quote` is the source of truth.

## Errors

| Error | Meaning |
|---|---|
| `MintClosed()` | mint is not open |
| `SoldOut()` | not enough supply left for `qty` |
| `WalletLimit()` | `mintedBy + qty > 5` |
| `BadQuantity()` | `qty == 0` |
| `WrongPayment(uint256 expected)` | `msg.value != quote`; re-quote and resend with `expected` |

## Examples

cast (Foundry):

```sh
C=0x5303C952dFE360D2c76f880447F2E0e3A7Fd0cF3   # CigPack
RPC=https://bsc-dataseed.binance.org
W=0xYourWallet
cast call $C "mintOpen()(bool)" --rpc-url $RPC
V=$(cast call $C "quote(address,uint256)(uint256)" $W 1 --rpc-url $RPC | awk '{print $1}')
cast send $C "mint(uint256)" 1 --value $V --rpc-url $RPC --private-key "$AGENT_KEY"   # key from your own secret store
```

ethers v6:

```js
const abi = [
  "function quote(address,uint256) view returns (uint256)",
  "function mint(uint256) payable",
  "function mintOpen() view returns (bool)",
];
const c = new ethers.Contract(CIGPACK, abi, signer);
if (!(await c.mintOpen())) throw new Error("mint closed");
const value = await c.quote(await signer.getAddress(), 1n);
const rc = await (await c.mint(1n, { value })).wait();
```

Raw call: `mint(uint256)` selector is `0xa0712d68`. Calldata for qty=1 is `0xa0712d680000000000000000000000000000000000000000000000000000000000000001`.

## After mint: reveal (only if your human says yes)

Every pack mints **sealed**. `tokenURI(id)` shows a placeholder with `Status: Sealed` until that pack is revealed. Each pack is revealed on its own, whenever its holder chooses. Sealed packs can be held, transferred, listed and sold like any other pack.

**Rule for agents:** after a mint, ask your human owner whether to reveal the new pack(s) now or keep them sealed. Do not call `requestReveal` or `reveal` unless they explicitly say yes. Keeping a pack sealed is a valid choice.

How it works: two calls, about `REVEAL_DELAY` (5) blocks apart. That's a few seconds on BNB Chain.

1. `requestReveal(uint256[] ids)`. Only the holder, or an address it approved, can call it, and only for packs that are still sealed. It locks each pack to the hash of block `block.number + 5`, which nobody knows yet. Emits `RevealRequested(id, revealBlock)`.
2. Wait until `block.number > revealBlock`. `revealState(id).status` turns from `1` (pending) to `2` (ready).
3. `reveal(uint256[] ids)`. **Anyone** can call it once the block is mined: you, your human, a buyer, a bot. It writes the pack's seed once and emits `Revealed(id, seed)`. `tokenURI(id)` now returns the final art and traits. Ids that are already revealed are skipped.

The result is fixed and can't be rerolled. A pack can be requested only once, ever. The seed is decided by the target block's hash and written once, and a transfer doesn't reset anything.

**Finish promptly.** If nobody calls `reveal` within 256 blocks of the target block (a few minutes on BNB Chain), the block hash is no longer readable on-chain. The pack is not reset and can't be requested again. Instead, `reveal` finishes it as a **Common** pack: rarity is forced to Common, and its other traits come from a fallback seed that was already fixed at request time (`fallbackSeedOf(id, revealBlock)`). Waiting therefore never gets a better pack. So once your human says yes, call `reveal` as soon as `block.number > revealBlock`. If you are interrupted, anyone can still finish it for you, and `revealState(id).status == 3` means the window has passed.

```solidity
function requestReveal(uint256[] ids)                     // holder or approved operator, sealed packs only
function reveal(uint256[] ids)                            // anyone, after block revealBlock is mined
function isRevealed(uint256 id) view returns (bool)
function revealBlockOf(uint256 id) view returns (uint256) // 0 = not requested
function revealState(uint256 id) view returns (tuple(uint8 status, uint64 revealBlock, uint32 seed))
// status: 0 sealed, 1 pending, 2 ready, 3 window passed (reveal now gives a Common pack), 4 revealed
function revealedLate(uint256 id) view returns (bool)     // true = finished after the window (Common)
function fallbackSeedOf(uint256 id, uint256 revealBlock) view returns (uint32)  // the late (Common) seed, fixed at request
function REVEAL_DELAY() view returns (uint256)            // 5
function REVEAL_WINDOW() view returns (uint256)           // 256
event RevealRequested(uint256 indexed id, uint256 revealBlock)
event Revealed(uint256 indexed id, uint32 seed)
```

| Error | Meaning |
|---|---|
| `NotPackOwner()` | `requestReveal` from a wallet that neither holds nor is approved for the pack |
| `AlreadyRequested()` | reveal already started; just call `reveal` |
| `AlreadyRevealed()` | pack is already revealed |
| `NotRequested()` | `reveal` on a pack whose holder hasn't started it |
| `RevealNotReady()` | target block not mined yet; wait a few seconds and retry |

cast (only after your human said yes):

```sh
ID=42
cast send $C "requestReveal(uint256[])" "[$ID]" --rpc-url $RPC --private-key "$AGENT_KEY"
RB=$(cast call $C "revealBlockOf(uint256)(uint256)" $ID --rpc-url $RPC | awk '{print $1}')
while [ "$(cast block-number --rpc-url $RPC)" -le "$RB" ]; do sleep 1; done   # then finish right away (256-block window)
cast send $C "reveal(uint256[])" "[$ID]" --rpc-url $RPC --private-key "$AGENT_KEY"
cast call $C "tokenURI(uint256)(string)" $ID --rpc-url $RPC   # base64 JSON with the onchain SVG
```

Several packs: pass them all in one array, e.g. `"[41,42,43]"`.

## Trade on the CigPack market

A small, ownerless market contract. The pack stays in the seller's wallet until it sells; the market only holds a per-pack approval. Buyers pay the exact list price. 5% ERC-2981 royalty goes to `royaltyInfo` receiver, the rest to the seller, in the same transaction. No platform fee.

- Market: `0xCf9cd094bEab73fb8d5096Ccb50b224A64f26043`

```solidity
function list(uint256 tokenId, uint256 price)          // you own it + market approved; price in wei > 0
function updatePrice(uint256 tokenId, uint256 price)   // seller only
function cancel(uint256 tokenId)                       // seller any time; anyone if the listing is stale
function buy(uint256 tokenId) payable                  // msg.value == price, exactly
function getListing(uint256 tokenId) view returns (tuple(uint256 tokenId, address seller, uint256 price, uint64 listedAt, bool valid))
function listings(uint256 offset, uint256 limit) view returns (tuple(uint256 tokenId, address seller, uint256 price, uint64 listedAt, bool valid)[])
function activeCount() view returns (uint256)
function isValid(uint256 tokenId) view returns (bool)  // seller still owns it and the market is still approved
function pendingWithdrawals(address) view returns (uint256)
function withdraw()                                     // only if a payout to you failed (e.g. a contract wallet that rejects BNB)
event Sold(uint256 indexed tokenId, address indexed seller, address indexed buyer, uint256 price, address royaltyReceiver, uint256 royalty)
```

Sell:

1. `approve(MARKET, tokenId)` on the CigPack contract. Use per-pack `approve`, not `setApprovalForAll`: it clears itself when the pack moves, so an old listing can't come back to life later.
2. `list(tokenId, priceWei)`.
3. Change it with `updatePrice`, or pull it with `cancel`.

Buy:

1. `getListing(tokenId)`. Skip it unless `valid` is true.
2. `buy(tokenId)` with `value = price` from that same read. If the seller changed the price in between, the call reverts with `WrongValue(expected)` instead of charging you more.

To browse, page through `listings(offset, 100)` until you've read `activeCount()`. Stale entries come back with `valid = false`; ignore them.

| Error | Meaning |
|---|---|
| `NotListed()` | No listing for that token |
| `ListingInvalid()` | Seller moved the pack or pulled approval |
| `WrongValue(uint256 expected)` | `msg.value != price`; re-read and resend |
| `SelfBuy()` | Seller and buyer are the same wallet |
| `NotTokenOwner()` / `NotApproved()` | Can't list: not yours, or the market isn't approved |
| `NotSeller()` | Only the seller can update or cancel a valid listing |
| `ZeroPrice()` | Price must be above 0 |

cast:

```sh
M=0xCf9cd094bEab73fb8d5096Ccb50b224A64f26043   # CigPackMarket
cast send $C "approve(address,uint256)" $M 42 --rpc-url $RPC --private-key "$AGENT_KEY"
cast send $M "list(uint256,uint256)" 42 $(cast to-wei 0.05) --rpc-url $RPC --private-key "$AGENT_KEY"
P=$(cast call $M "getListing(uint256)((uint256,address,uint256,uint64,bool))" 42 --rpc-url $RPC | cut -d, -f3 | awk '{print $1}')
cast send $M "buy(uint256)" 42 --value $P --rpc-url $RPC --private-key "$BUYER_KEY"
```

Ask your human before you list or buy anything, and show them the price first.

Other BNB Chain marketplaces that trade CigPack today: Element (`https://element.market/assets/bsc/<contract>/<tokenId>`).

