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.
Token ID Formula
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)
└── cancelAuction2. Smart Contract API Reference
Contract: MillionFinneyHomepage.sol — ERC-721 with on-chain metadata
Write Functions
purchasePixel
purchasePixel(uint256 tokenId, string title, bytes3 color) payableBuy a single pixel. Media is set separately via setPixelMedia. Excess ETH is refunded. Reverts if already owned or underpaid.
- 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)
setPixelMedia
setPixelMedia(uint256 tokenId, string mediaURI)Set the media URI for a purchased pixel. Owner-only, one-time, immutable. Call this after purchasePixel.
- tokenId (uint256) — Pixel ID (must own)
- mediaURI (string) — IPFS URI (e.g. ipfs://Qm...)
purchasePixelBatch
purchasePixelBatch(uint256[] tokenIds, string[] titles, bytes3[] colors) payableBuy up to 100 pixels in one transaction. Media is set separately via setPixelMedia for each pixel. All arrays must have equal length.
- tokenIds (uint256[]) — Array of pixel IDs (max 100)
- titles (string[]) — One title per pixel
- colors (bytes3[]) — One packed RGB color per pixel
createAuction
createAuction(uint256 tokenId, uint256 startPrice, uint256 endPrice, uint256 duration)List a pixel for Dutch auction. Only callable by owner, only after allPixelsSold == true.
- 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) payablePurchase at current Dutch auction price. Overpayment refunded. 2.5% platform fee deducted from sale.
- tokenId (uint256) — Pixel with active auction
cancelAuction
cancelAuction(uint256 tokenId)Cancel your active auction. Only callable by the pixel owner.
- tokenId (uint256) — Pixel ID with active auction
Read Functions
isPixelOwned
isPixelOwned(uint256 tokenId) → boolCheck if a pixel has been purchased. Use this before calling purchasePixel to avoid reverts.
- tokenId (uint256) — Pixel ID to check
getPixelData
getPixelData(uint256 tokenId) → (string, string, string, uint256, address)Returns immutable pixel metadata. Reverts if pixel doesn't exist.
- tokenId (uint256) — Pixel ID
getCurrentAuctionPrice
getCurrentAuctionPrice(uint256 tokenId) → uint256Current Dutch auction price. Decreases linearly from startPrice to endPrice over duration.
- tokenId (uint256) — Pixel with active auction
getAuction
getAuction(uint256 tokenId) → (address, uint256, uint256, uint256, uint256, bool)Full auction struct for a pixel.
- tokenId (uint256) — Pixel ID
getPixelCoordinates
getPixelCoordinates(uint256 tokenId) → (uint256 x, uint256 y)Convert token ID to grid coordinates.
- tokenId (uint256) — Token ID (0–999999)
getTokenId
getTokenId(uint256 x, uint256 y) → uint256Convert grid coordinates to token ID.
- x (uint256) — Column (0–999)
- y (uint256) — Row (0–999)
getPixelColors
getPixelColors(uint256 startId, uint256 count) → (bytes3[], bool[])Batch read for grid rendering. Returns arrays of packed RGB colors and ownership flags.
- startId (uint256) — Starting token ID
- count (uint256) — Number of pixels
tokenURI
tokenURI(uint256 tokenId) → stringReturns fully on-chain Base64-encoded JSON metadata. No external server required.
- tokenId (uint256) — Token ID (must exist)
pixelsSold
pixelsSold() → uint256Total number of pixels purchased so far.
allPixelsSold
allPixelsSold() → boolWhether all 1,000,000 pixels have been sold. Auctions are only enabled when this is true.
Constants
| Name | Value | Description |
|---|---|---|
| PIXEL_PRICE | 1e15 (0.001 ETH) | Cost per pixel in wei |
| GRID_WIDTH | 1000 | Grid columns |
| GRID_HEIGHT | 1000 | Grid rows |
| TOTAL_PIXELS | 1,000,000 | Total pixels |
| AUCTION_FEE_BPS | 250 | 2.5% fee on auctions |
| MIN_AUCTION_DURATION | 3600 | 1 hour |
| MAX_AUCTION_DURATION | 2592000 | 30 days |
Events
| Event | Parameters |
|---|---|
| PixelPurchased | tokenId (indexed), buyer (indexed), title, color, timestamp |
| PixelMediaSet | tokenId (indexed), owner (indexed), mediaURI |
| AuctionCreated | tokenId (indexed), seller (indexed), startPrice, endPrice, duration |
| AuctionSettled | tokenId (indexed), seller, buyer, price |
| AuctionCancelled | tokenId (indexed), seller |
3. Bot Scripts — Ethers.js
Copy-paste examples for building purchase bots, snipers, and monitoring scripts with Ethers.js v6.
Setup
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
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)
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
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
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 ETHRead On-Chain Metadata
// 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
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
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
// 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
// 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
// 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 gridBulk Metadata Fetcher
// 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
// 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
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
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
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
setPixelMediaafter purchase — one-time, immutable
Reading Media
// 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
// 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
// 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.