Skip to content

API & Integration Documentation

1. Quick Start

The Million Finney Homepage is a 1000×1000 pixel grid. Each pixel costs 1 finney (0.001 ETH) and is an ERC-721 NFT with immutable metadata. This documentation covers programmatic interaction for bots, scripts, and integrations.

Grid
1000 × 1000
Price
0.001 ETH
Token ID
y * 1000 + x
Batch Limit
100 per tx
Metadata
On-Chain
Auction Fee
2.5%

Token ID Formula

text
tokenId = y * 1000 + x

x = column (0–999), y = row (0–999)

(0, 0)     → 0         (top-left)
(500, 250) → 250500
(999, 999) → 999999    (bottom-right)

Pixel Lifecycle

AVAILABLE ──► purchasePixel (0.001 ETH) ──► OWNED (NFT minted, data immutable)
                                               │
                                               └── allPixelsSold == true?
                                                     │
                                                     YES ──► createAuction (Dutch auction)
                                                                 ├── Price decreases linearly
                                                                 ├── buyFromAuction (2.5% fee)
                                                                 └── cancelAuction

2. Smart Contract API Reference

Contract: MillionFinneyHomepage.sol — ERC-721 with on-chain metadata

Write Functions

purchasePixel

purchasePixel(uint256 tokenId, string title, bytes3 color) payable

Buy a single pixel. Media is set separately via setPixelMedia. Excess ETH is refunded. Reverts if already owned or underpaid.

Params:
  • tokenId (uint256)y * 1000 + x (0–999999)
  • title (string)Pixel title (immutable, non-empty)
  • color (bytes3)Packed RGB color for grid rendering (e.g. 0xFF5733)
Value: 0.001 ETH minimum

setPixelMedia

setPixelMedia(uint256 tokenId, string mediaURI)

Set the media URI for a purchased pixel. Owner-only, one-time, immutable. Call this after purchasePixel.

Params:
  • tokenId (uint256)Pixel ID (must own)
  • mediaURI (string)IPFS URI (e.g. ipfs://Qm...)

purchasePixelBatch

purchasePixelBatch(uint256[] tokenIds, string[] titles, bytes3[] colors) payable

Buy up to 100 pixels in one transaction. Media is set separately via setPixelMedia for each pixel. All arrays must have equal length.

Params:
  • tokenIds (uint256[])Array of pixel IDs (max 100)
  • titles (string[])One title per pixel
  • colors (bytes3[])One packed RGB color per pixel
Value: 0.001 ETH × count

createAuction

createAuction(uint256 tokenId, uint256 startPrice, uint256 endPrice, uint256 duration)

List a pixel for Dutch auction. Only callable by owner, only after allPixelsSold == true.

Params:
  • tokenId (uint256)Pixel ID (must own)
  • startPrice (uint256)Starting price in wei
  • endPrice (uint256)Floor price in wei (can be 0)
  • duration (uint256)Seconds (3600–2592000)

buyFromAuction

buyFromAuction(uint256 tokenId) payable

Purchase at current Dutch auction price. Overpayment refunded. 2.5% platform fee deducted from sale.

Params:
  • tokenId (uint256)Pixel with active auction
Value: ≥ getCurrentAuctionPrice(tokenId)

cancelAuction

cancelAuction(uint256 tokenId)

Cancel your active auction. Only callable by the pixel owner.

Params:
  • tokenId (uint256)Pixel ID with active auction

Read Functions

isPixelOwned

isPixelOwned(uint256 tokenId) → bool

Check if a pixel has been purchased. Use this before calling purchasePixel to avoid reverts.

Params:
  • tokenId (uint256)Pixel ID to check
Returns: bool — true if owned

getPixelData

getPixelData(uint256 tokenId) → (string, string, string, uint256, address)

Returns immutable pixel metadata. Reverts if pixel doesn't exist.

Params:
  • tokenId (uint256)Pixel ID
Returns: (title, mediaURI, color, purchaseTimestamp, originalBuyer)

getCurrentAuctionPrice

getCurrentAuctionPrice(uint256 tokenId) → uint256

Current Dutch auction price. Decreases linearly from startPrice to endPrice over duration.

Params:
  • tokenId (uint256)Pixel with active auction
Returns: uint256 — price in wei

getAuction

getAuction(uint256 tokenId) → (address, uint256, uint256, uint256, uint256, bool)

Full auction struct for a pixel.

Params:
  • tokenId (uint256)Pixel ID
Returns: (seller, startPrice, endPrice, startTime, duration, active)

getPixelCoordinates

getPixelCoordinates(uint256 tokenId) → (uint256 x, uint256 y)

Convert token ID to grid coordinates.

Params:
  • tokenId (uint256)Token ID (0–999999)
Returns: (x, y)

getTokenId

getTokenId(uint256 x, uint256 y) → uint256

Convert grid coordinates to token ID.

Params:
  • x (uint256)Column (0–999)
  • y (uint256)Row (0–999)
Returns: uint256 — tokenId

getPixelColors

getPixelColors(uint256 startId, uint256 count) → (bytes3[], bool[])

Batch read for grid rendering. Returns arrays of packed RGB colors and ownership flags.

Params:
  • startId (uint256)Starting token ID
  • count (uint256)Number of pixels
Returns: (bytes3[] colors, bool[] owned)

tokenURI

tokenURI(uint256 tokenId) → string

Returns fully on-chain Base64-encoded JSON metadata. No external server required.

Params:
  • tokenId (uint256)Token ID (must exist)
Returns: string — "data:application/json;base64,..."

pixelsSold

pixelsSold() → uint256

Total number of pixels purchased so far.

Returns: uint256 (0–1000000)

allPixelsSold

allPixelsSold() → bool

Whether all 1,000,000 pixels have been sold. Auctions are only enabled when this is true.

Returns: bool

Constants

NameValueDescription
PIXEL_PRICE1e15 (0.001 ETH)Cost per pixel in wei
GRID_WIDTH1000Grid columns
GRID_HEIGHT1000Grid rows
TOTAL_PIXELS1,000,000Total pixels
AUCTION_FEE_BPS2502.5% fee on auctions
MIN_AUCTION_DURATION36001 hour
MAX_AUCTION_DURATION259200030 days

Events

EventParameters
PixelPurchasedtokenId (indexed), buyer (indexed), title, color, timestamp
PixelMediaSettokenId (indexed), owner (indexed), mediaURI
AuctionCreatedtokenId (indexed), seller (indexed), startPrice, endPrice, duration
AuctionSettledtokenId (indexed), seller, buyer, price
AuctionCancelledtokenId (indexed), seller

3. Bot Scripts — Ethers.js

Copy-paste examples for building purchase bots, snipers, and monitoring scripts with Ethers.js v6.

Setup

typescript
import { ethers } from "ethers";

const provider = new ethers.JsonRpcProvider("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY");
const wallet = new ethers.Wallet("YOUR_PRIVATE_KEY", provider);

const ABI = [
  "function purchasePixel(uint256 tokenId, string title, bytes3 color) payable",
  "function purchasePixelBatch(uint256[] tokenIds, string[] titles, bytes3[] colors) payable",
  "function setPixelMedia(uint256 tokenId, string mediaURI)",
  "function isPixelOwned(uint256 tokenId) view returns (bool)",
  "function getPixelData(uint256 tokenId) view returns (string, string, bytes3, uint256, address)",
  "function getCurrentAuctionPrice(uint256 tokenId) view returns (uint256)",
  "function getAuction(uint256 tokenId) view returns (address, uint256, uint256, uint256, uint256, bool)",
  "function buyFromAuction(uint256 tokenId) payable",
  "function createAuction(uint256 tokenId, uint256 startPrice, uint256 endPrice, uint256 duration)",
  "function cancelAuction(uint256 tokenId)",
  "function getPixelColors(uint256 startId, uint256 count) view returns (bytes3[], bool[])",
  "function pixelsSold() view returns (uint256)",
  "function allPixelsSold() view returns (bool)",
  "function ownerOf(uint256 tokenId) view returns (address)",
  "function tokenURI(uint256 tokenId) view returns (string)",
  "event PixelPurchased(uint256 indexed tokenId, address indexed buyer, string title, bytes3 color, uint256 timestamp)",
  "event PixelMediaSet(uint256 indexed tokenId, address indexed owner, string mediaURI)",
  "event AuctionCreated(uint256 indexed tokenId, address indexed seller, uint256 startPrice, uint256 endPrice, uint256 duration)",
  "event AuctionSettled(uint256 indexed tokenId, address seller, address buyer, uint256 price)",
];

const CONTRACT = "0x..."; // deployed contract address
const contract = new ethers.Contract(CONTRACT, ABI, wallet);

Buy a Single Pixel

typescript
async function buyPixel(x: number, y: number, title: string, color: string) {
  const tokenId = y * 1000 + x;

  const owned = await contract.isPixelOwned(tokenId);
  if (owned) throw new Error(`Pixel (${x}, ${y}) already owned`);

  const tx = await contract.purchasePixel(
    tokenId, title, color,
    { value: ethers.parseEther("0.001") }
  );
  const receipt = await tx.wait();
  console.log(`Purchased (${x}, ${y}) — tx: ${receipt.hash}`);
  return receipt;
}

// Purchase and then set media separately
await buyPixel(500, 250, "My Bot Pixel", "0xFF5733");
// After purchase, set media:
const mediaUri = "ipfs://QmHash"; // upload via /api/ipfs/upload
await contract.setPixelMedia(250500, mediaUri);

Batch Purchase (up to 100)

typescript
async function batchBuy(
  pixels: { x: number; y: number; title: string; color: string }[]
) {
  if (pixels.length > 100) throw new Error("Max 100 per batch");

  const tokenIds = pixels.map(p => p.y * 1000 + p.x);
  const titles = pixels.map(p => p.title);
  const colors = pixels.map(p => p.color);
  const value = ethers.parseEther((0.001 * pixels.length).toFixed(3));

  const tx = await contract.purchasePixelBatch(tokenIds, titles, colors, { value });
  const receipt = await tx.wait();
  console.log(`Batch purchased ${pixels.length} pixels — tx: ${receipt.hash}`);
  return receipt;
}

// Buy a 10×10 block starting at (100, 100)
const block = [];
for (let dy = 0; dy < 10; dy++) {
  for (let dx = 0; dx < 10; dx++) {
    block.push({
      x: 100 + dx, y: 100 + dy,
      title: `Block (${100 + dx}, ${100 + dy})`,
      color: "0x3366FF",
    });
  }
}
await batchBuy(block);

// Then set media for each pixel individually
for (const p of block) {
  const tokenId = p.y * 1000 + p.x;
  await contract.setPixelMedia(tokenId, "ipfs://QmYourHash");
}

Find Available Pixels in a Region

typescript
async function findAvailable(startX: number, startY: number, width: number, height: number) {
  const available: number[] = [];

  for (let y = startY; y < startY + height; y++) {
    const rowStart = y * 1000 + startX;
    const [, owned] = await contract.getPixelColors(rowStart, width);

    for (let i = 0; i < width; i++) {
      if (!owned[i]) available.push(rowStart + i);
    }
  }

  console.log(`Found ${available.length} available pixels in region`);
  return available;
}

// Scan a 50×50 area
const free = await findAvailable(200, 300, 50, 50);
console.log("Available token IDs:", free.slice(0, 10), "...");

Auction Sniper Bot

typescript
async function sniperBot(maxPriceEth: number) {
  console.log(`Sniper active — max price: ${maxPriceEth} ETH`);
  const maxPrice = ethers.parseEther(maxPriceEth.toString());

  contract.on("AuctionCreated", async (tokenId, seller, startPrice, endPrice, duration) => {
    // Skip if floor price exceeds our max
    if (endPrice > maxPrice) return;

    console.log(`New auction: pixel #${tokenId} — ${ethers.formatEther(startPrice)} → ${ethers.formatEther(endPrice)} ETH`);

    // Poll price until it drops below our max
    const interval = setInterval(async () => {
      try {
        const price = await contract.getCurrentAuctionPrice(tokenId);
        console.log(`  #${tokenId} current price: ${ethers.formatEther(price)} ETH`);

        if (price <= maxPrice) {
          clearInterval(interval);
          const tx = await contract.buyFromAuction(tokenId, { value: price });
          const receipt = await tx.wait();
          console.log(`  BOUGHT #${tokenId} for ${ethers.formatEther(price)} ETH — tx: ${receipt.hash}`);
        }
      } catch {
        clearInterval(interval); // auction ended or cancelled
      }
    }, 30_000); // check every 30s
  });
}

await sniperBot(0.05); // buy any auction that drops below 0.05 ETH

Read On-Chain Metadata

typescript
// Decode fully on-chain tokenURI
async function getMetadata(tokenId: number) {
  const uri = await contract.tokenURI(tokenId);
  const json = JSON.parse(atob(uri.replace("data:application/json;base64,", "")));
  return json; // { name, description, image, attributes: [...] }
}

const meta = await getMetadata(250500);
console.log(meta.name);       // "Pixel (500, 250)"
console.log(meta.attributes); // [{ trait_type: "X", value: "500" }, ...]

4. Bot Scripts — Viem

Setup

typescript
import { createPublicClient, createWalletClient, http, parseEther, parseAbi } from "viem";
import { mainnet } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";

const abi = parseAbi([
  "function purchasePixel(uint256 tokenId, string title, bytes3 color) payable",
  "function purchasePixelBatch(uint256[] tokenIds, string[] titles, bytes3[] colors) payable",
  "function setPixelMedia(uint256 tokenId, string mediaURI)",
  "function isPixelOwned(uint256 tokenId) view returns (bool)",
  "function getPixelData(uint256 tokenId) view returns (string, string, bytes3, uint256, address)",
  "function getCurrentAuctionPrice(uint256 tokenId) view returns (uint256)",
  "function getAuction(uint256 tokenId) view returns (address, uint256, uint256, uint256, uint256, bool)",
  "function buyFromAuction(uint256 tokenId) payable",
  "function pixelsSold() view returns (uint256)",
  "function ownerOf(uint256 tokenId) view returns (address)",
  "event PixelPurchased(uint256 indexed tokenId, address indexed buyer, string title, bytes3 color, uint256 timestamp)",
  "event PixelMediaSet(uint256 indexed tokenId, address indexed owner, string mediaURI)",
  "event AuctionCreated(uint256 indexed tokenId, address indexed seller, uint256 startPrice, uint256 endPrice, uint256 duration)",
  "event AuctionSettled(uint256 indexed tokenId, address seller, address buyer, uint256 price)",
]);

const CONTRACT = "0x..." as const;

const publicClient = createPublicClient({
  chain: mainnet,
  transport: http("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY"),
});

const account = privateKeyToAccount("0xYOUR_PRIVATE_KEY");
const walletClient = createWalletClient({
  account,
  chain: mainnet,
  transport: http("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY"),
});

Purchase

typescript
const tokenId = 250500n; // (500, 250)

const owned = await publicClient.readContract({
  address: CONTRACT, abi,
  functionName: "isPixelOwned",
  args: [tokenId],
});

if (!owned) {
  const hash = await walletClient.writeContract({
    address: CONTRACT, abi,
    functionName: "purchasePixel",
    args: [tokenId, "My Pixel", "0xFF5733"],
    value: parseEther("0.001"),
  });
  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  console.log("Purchased!", receipt.transactionHash);
}

Watch Events

typescript
// Real-time purchase monitoring
const unwatch = publicClient.watchContractEvent({
  address: CONTRACT, abi,
  eventName: "PixelPurchased",
  onLogs: (logs) => {
    for (const log of logs) {
      const { tokenId, buyer, title, color } = log.args;
      const x = Number(tokenId) % 1000;
      const y = Math.floor(Number(tokenId) / 1000);
      console.log(`[${new Date().toISOString()}] (${x}, ${y}) purchased by ${buyer} — "${title}" ${color}`);
    }
  },
});

// Query historical purchases
const logs = await publicClient.getContractEvents({
  address: CONTRACT, abi,
  eventName: "PixelPurchased",
  fromBlock: 0n,
});
console.log(`${logs.length} total purchases`);

5. Automation Recipes

Common bot patterns for automated interaction with the contract.

Grid Scanner — Export Full State

typescript
// Scan the entire grid in chunks and export to JSON
async function exportGrid() {
  const CHUNK = 1000;
  const grid: Record<number, { color: string; owned: boolean }> = {};

  for (let start = 0; start < 1_000_000; start += CHUNK) {
    const [colors, owned] = await contract.getPixelColors(start, CHUNK);
    for (let i = 0; i < CHUNK; i++) {
      if (owned[i]) {
        grid[start + i] = { color: colors[i], owned: true };
      }
    }
    process.stdout.write(`\rScanned ${start + CHUNK} / 1,000,000`);
  }

  const fs = await import("fs");
  fs.writeFileSync("grid-state.json", JSON.stringify(grid, null, 2));
  console.log(`\nExported ${Object.keys(grid).length} owned pixels`);
}

await exportGrid();

Availability Monitor

typescript
// Watch a region and alert when pixels become available (via auction settlement)
async function monitorRegion(x0: number, y0: number, w: number, h: number) {
  console.log(`Monitoring (${x0},${y0}) to (${x0+w-1},${y0+h-1})...`);

  const targetIds = new Set<number>();
  for (let y = y0; y < y0 + h; y++) {
    for (let x = x0; x < x0 + w; x++) {
      targetIds.add(y * 1000 + x);
    }
  }

  contract.on("AuctionCreated", (tokenId: bigint) => {
    if (targetIds.has(Number(tokenId))) {
      console.log(`🔔 AUCTION in monitored region: pixel #${tokenId}`);
    }
  });
}

await monitorRegion(400, 400, 200, 200); // watch center of grid

Bulk Metadata Fetcher

typescript
// Fetch metadata for all owned pixels in a range
async function fetchMetadata(startId: number, count: number) {
  const results: { tokenId: number; title: string; owner: string; color: string }[] = [];

  const [, owned] = await contract.getPixelColors(startId, count);

  for (let i = 0; i < count; i++) {
    if (!owned[i]) continue;
    const id = startId + i;
    const [title, , color] = await contract.getPixelData(id);
    const owner = await contract.ownerOf(id);
    results.push({ tokenId: id, title, owner, color });
  }

  return results;
}

const data = await fetchMetadata(0, 100);
console.table(data);

Gas-Optimized Batch Strategy

typescript
// Split large purchases into optimal batches
async function buyMany(pixelArgs: { x: number; y: number; title: string; color: string }[]) {
  const BATCH_SIZE = 100;

  for (let i = 0; i < pixelArgs.length; i += BATCH_SIZE) {
    const batch = pixelArgs.slice(i, i + BATCH_SIZE);
    const tokenIds = batch.map(p => p.y * 1000 + p.x);
    const value = ethers.parseEther((0.001 * batch.length).toFixed(3));

    console.log(`Batch ${Math.floor(i / BATCH_SIZE) + 1}: ${batch.length} pixels`);

    const tx = await contract.purchasePixelBatch(
      tokenIds,
      batch.map(p => p.title),
      batch.map(p => p.color),
      { value }
    );
    await tx.wait();
    console.log(`  ✓ Confirmed`);
  }

  // Set media for each purchased pixel
  for (const p of pixelArgs) {
    const tokenId = p.y * 1000 + p.x;
    const tx = await contract.setPixelMedia(tokenId, "ipfs://QmYourMediaHash");
    await tx.wait();
  }

  console.log(`Done — ${pixelArgs.length} pixels purchased`);
}

6. IPFS Upload API

Media files (images, audio, and video) are stored on IPFS via Filebase. The app exposes a simple upload endpoint. Only the IPFS CID is stored on-chain via setPixelMedia.

Endpoint

POST /api/ipfs/upload

Content-Type: multipart/form-data

cURL

bash
curl -X POST https://your-domain.com/api/ipfs/upload \
  -F "[email protected]" \
  -F "address=0xYourWalletAddress" \
  -F "signature=0xSignedMessage" \
  -F "tokenId=42"

# Response: { "cid": "QmYourHash", "uri": "ipfs://QmYourHash", "key": "..." }

JavaScript / Node.js

typescript
async function uploadMedia(
  filePath: string,
  address: string,
  signature: string,
  tokenId: number
): Promise<string> {
  const fs = await import("fs");
  const formData = new FormData();
  formData.append("file", new Blob([fs.readFileSync(filePath)]));
  formData.append("address", address);
  formData.append("signature", signature);
  formData.append("tokenId", tokenId.toString());

  const res = await fetch("https://your-domain.com/api/ipfs/upload", {
    method: "POST",
    body: formData,
  });

  const { uri } = await res.json();
  return uri; // "ipfs://QmYourHash"
}

// Sign the upload message first (message format includes tokenId)
const message = `Upload media for Million Finney Homepage pixel #${tokenId}`;
const signature = await wallet.signMessage(message);

// Upload media and set it on a purchased pixel
const mediaURI = await uploadMedia("./my-pixel.png", wallet.address, signature, tokenId);
await contract.setPixelMedia(tokenId, mediaURI);

Python

python
import requests

def upload_media(file_path: str, address: str, signature: str, token_id: int) -> str:
    with open(file_path, "rb") as f:
        res = requests.post(
            "https://your-domain.com/api/ipfs/upload",
            files={"file": f},
            data={
                "address": address,
                "signature": signature,
                "tokenId": str(token_id),
            },
        )
    return res.json()["uri"]  # "ipfs://QmYourHash"

uri = upload_media("pixel.png", wallet_address, signature, 42)
print(f"Uploaded: {uri}")

Media Requirements

  • Image formats: PNG, JPEG, GIF, WebP
  • Audio formats: MP3, WAV, OGG, AAC
  • Video formats: MP4, WebM
  • Max size: 5 MB
  • Media is set via setPixelMedia after purchase — one-time, immutable

Reading Media

typescript
// Convert on-chain IPFS URI to a gateway URL
function ipfsToHttp(uri: string): string {
  const gateway = "https://cloudflare-ipfs.com/ipfs/";
  return uri.replace("ipfs://", gateway);
}

const [, mediaURI] = await contract.getPixelData(250500);
const httpUrl = ipfsToHttp(mediaURI);
// → "https://cloudflare-ipfs.com/ipfs/QmYourHash"

7. Event Monitoring

Stream contract events for real-time dashboards, analytics, or alerting.

Ethers.js — Live Listener

typescript
// Purchase stream
contract.on("PixelPurchased", (tokenId, buyer, title, color, timestamp) => {
  const x = Number(tokenId) % 1000;
  const y = Math.floor(Number(tokenId) / 1000);
  console.log(JSON.stringify({ event: "purchase", x, y, buyer, title, color, timestamp: Number(timestamp) }));
});

// Media set stream
contract.on("PixelMediaSet", (tokenId, owner, mediaURI) => {
  console.log(JSON.stringify({ event: "media_set", tokenId: Number(tokenId), owner, mediaURI }));
});

// Auction stream
contract.on("AuctionCreated", (tokenId, seller, startPrice, endPrice, duration) => {
  console.log(JSON.stringify({
    event: "auction_created",
    tokenId: Number(tokenId),
    seller,
    startPrice: ethers.formatEther(startPrice),
    endPrice: ethers.formatEther(endPrice),
    durationSec: Number(duration),
  }));
});

contract.on("AuctionSettled", (tokenId, seller, buyer, price) => {
  console.log(JSON.stringify({
    event: "auction_settled",
    tokenId: Number(tokenId),
    seller, buyer,
    priceEth: ethers.formatEther(price),
  }));
});

Historical Event Query

typescript
// Fetch all past purchases
const filter = contract.filters.PixelPurchased();
const events = await contract.queryFilter(filter, 0, "latest");

console.log(`Total purchases: ${events.length}`);

// Build ownership map
const owners = new Map<number, string>();
for (const e of events) {
  owners.set(Number(e.args.tokenId), e.args.buyer);
}

// Export as CSV
const csv = ["tokenId,x,y,buyer,title,color"];
for (const e of events) {
  const id = Number(e.args.tokenId);
  csv.push(`${id},${id % 1000},${Math.floor(id / 1000)},${e.args.buyer},"${e.args.title}",${e.args.color}`);
}
require("fs").writeFileSync("purchases.csv", csv.join("\n"));

8. AI Agent Guide

We provide a dedicated guide for AI agents, bots, and automated systems that want to interact with the Million Finney Homepage — whether for advertising, artistic expression, or trading.

🤖

Agent-Friendly Markdown Guide

A comprehensive, machine-readable guide covering advertising strategies, batch purchases, IPFS uploads, code examples, and strategic tips for grid positioning.

URL: /agent.md

Format: Markdown — optimized for LLM context windows

Contents: Project overview, API reference, code examples (Ethers.js & Viem), IPFS upload guide, strategic tips, rules

What's in the Agent Guide?

🏢 Advertising & Branding

How to purchase pixel blocks for brand presence, logo placement, and permanent on-chain advertising.

🎨 Artistic Expression

Creating pixel art, collaborative patterns, and visual compositions across the grid.

📋 Full API Reference

Every contract function, IPFS upload endpoint, event format, and constant — in one file.

💡 Strategic Tips

Grid positioning strategy, color selection, batch purchase patterns, and title best practices.


9. FAQ

Q: Can I modify pixel data after purchase?

Title and color are permanently immutable once purchased. Media (image, audio, or video) can be set once after purchase via setPixelMedia, and is then immutable forever.

Q: Can I transfer a pixel directly?

No. transferFrom and safeTransferFrom are blocked. Ownership only changes via Dutch auctions after all pixels are sold.

Q: What's the maximum batch size?

100 pixels per purchasePixelBatch call. For larger purchases, split into multiple transactions.

Q: When are auctions enabled?

Only after allPixelsSold() returns true (all 1,000,000 pixels purchased).

Q: How does the Dutch auction price work?

Price decreases linearly from startPrice to endPrice over duration seconds. Call getCurrentAuctionPrice(tokenId) to get the current price. A 2.5% fee is deducted from the sale.

Q: Is there a rate limit on the IPFS upload API?

The /api/ipfs/upload endpoint accepts files up to 5 MB (images, audio, and video). There is no hard rate limit, but excessive usage may be throttled. For bulk uploads, add a small delay between requests.

Q: Can I use my own IPFS pinning service?

Yes. The contract accepts any valid ipfs:// URI. You can pin media with Pinata, Infura, web3.storage, or any IPFS provider — just pass the URI to setPixelMedia.

Q: Where is the contract deployed?

Check the deployment artifacts in packages/hardhat/deployments/ or query packages/nextjs/contracts/deployedContracts.ts for the address on each network.

Q: Can bots front-run purchases?

Pixel purchases are first-come-first-served on-chain. If two transactions target the same pixel, the first confirmed transaction wins and the second reverts. Use higher gas priority for competitive pixels.