# NFT Buy Drops — Brain On BNB AI # The contract, the metadata server and the collection page. # # This is the complete nft-drop bundle as a single file, so it can be read in # one fetch. 10 files, 1250 lines. # Download as a zip: https://brainonbnb.com/code/nft-drop.zip # Everything else: https://brainonbnb.com/code/index.txt # # No secrets are present: they are supplied at runtime through the environment. # MIT licensed. ============================================================================== === FILE: dashboard/nft/index.html ============================================================================== BOBAI Buy Drops · NFT Collection · brainonbnb
NICE BUY motif
💰
NICE BUY
$100+ · 1000 max
BIG BUY motif
💎
BIG BUY
$150+ · 500 max
HUGE BUY motif
🚀
HUGE BUY
$250+ · 250 max
WHALE BUY motif
🐋
WHALE BUY
$500+ · 100 max
THUNDER BUY motif
THUNDER BUY
$1000+ · 50 max
KRAKEN BUY motif
🦑
KRAKEN BUY
$2500+ · 25 max

BOBAI Buy Drops

Every $BOBAI buy of $100 or more auto-mints a collectible NFT to the buyer's wallet — no claim, no extra cost. Each NFT carries a buy-tier motif fixed by purchase size and a rarity drawn from a weighted matrix. 1,925 total · 6 tiers · 7 rarities.

Auto-mint live
Total Minted
Supply Cap
1,925
Unique Holders
Status
● Live

Tier availability

Each buy-tier and its per-rarity breakdown — minted / cap and your chance per buy. Every cell has a fixed cap; once it's full, that rarity stops for that tier. The bar shows how full the whole tier is.

Loading on-chain data…

Rarity distribution

Live count of each rarity across all 6 tiers, against its fixed collection-wide cap. Every rarity fills toward its target as the collection mints out.

Loading…

Live drops

Most recent mints from the on-chain BuyDrop event log. Newest first.

#TierRarityRecipientTimeTx
Loading recent mints…

The collection

All 42 possible card variants (6 motifs × 7 rarities). Each NFT minted matches one of these combinations.

============================================================================== === FILE: nft/contract/scripts/build-verify-input.js ============================================================================== // Build a Standard-JSON-Input file for BscScan contract verification. // Output: nft/contract/build/verify-input.json // Workflow: paste/upload into bscscan.com/verifyContract — "Solidity (Standard-Json-Input)" mode. // // Keys in `sources` MUST match the import paths solc sees, NOT the disk paths. // E.g. "@openzeppelin/contracts/utils/Context.sol" — not "node_modules/...". const fs = require('fs'); const path = require('path'); const ROOT = path.resolve(__dirname, '..'); const SRC_FILE = path.join(ROOT, 'src', 'BobaiBuyDrops.sol'); const OUT_FILE = path.join(ROOT, 'build', 'verify-input.json'); const sources = {}; // virtualPath = the key as solc/BscScan see it // absPath = disk location to read from function readSource(virtualPath, absPath) { if (sources[virtualPath]) return; const content = fs.readFileSync(absPath, 'utf8'); sources[virtualPath] = { content }; const importRegex = /^\s*import\s+(?:[^"']*from\s+)?["']([^"']+)["']/gm; let m; while ((m = importRegex.exec(content)) !== null) { const importPath = m[1]; let nextVirtual, nextAbs; if (importPath.startsWith('@openzeppelin/')) { nextVirtual = importPath; nextAbs = path.join(ROOT, 'node_modules', importPath); } else if (importPath.startsWith('./') || importPath.startsWith('../')) { // Relative import — resolve against the CURRENT file's virtual path const dirVirtual = path.posix.dirname(virtualPath); nextVirtual = path.posix.normalize(path.posix.join(dirVirtual, importPath)); nextAbs = path.resolve(path.dirname(absPath), importPath); } else { continue; } if (!fs.existsSync(nextAbs)) { console.warn(`! Missing import: ${importPath} (from ${virtualPath}) -> ${nextAbs}`); continue; } readSource(nextVirtual, nextAbs); } } readSource('BobaiBuyDrops.sol', SRC_FILE); const input = { language: 'Solidity', sources, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { '*': { '*': ['abi', 'evm.bytecode', 'evm.deployedBytecode', 'evm.methodIdentifiers', 'metadata'] }, }, }, }; fs.writeFileSync(OUT_FILE, JSON.stringify(input, null, 2)); console.log(`OK -> ${OUT_FILE}`); console.log(`Sources (${Object.keys(sources).length} files):`); Object.keys(sources).sort().forEach(k => console.log(' - ' + k)); console.log(`Total size: ${(fs.statSync(OUT_FILE).size / 1024).toFixed(1)} KB`); ============================================================================== === FILE: nft/contract/scripts/compile.js ============================================================================== // Compile BobaiBuyDrops.sol via solc-js with @openzeppelin import resolution. // Output: build/BobaiBuyDrops.json (abi + bytecode). const fs = require('fs'); const path = require('path'); const solc = require('solc'); const ROOT = path.resolve(__dirname, '..'); const SRC = path.join(ROOT, 'src', 'BobaiBuyDrops.sol'); const OUT = path.join(ROOT, 'build'); if (!fs.existsSync(OUT)) fs.mkdirSync(OUT, { recursive: true }); const source = fs.readFileSync(SRC, 'utf8'); function findImports(importPath) { // Resolve @openzeppelin/* via node_modules. if (importPath.startsWith('@openzeppelin/')) { const p = path.join(ROOT, 'node_modules', importPath); if (fs.existsSync(p)) return { contents: fs.readFileSync(p, 'utf8') }; } return { error: 'File not found: ' + importPath }; } const input = { language: 'Solidity', sources: { 'BobaiBuyDrops.sol': { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { '*': { '*': ['abi', 'evm.bytecode.object', 'evm.deployedBytecode.object'] }, }, }, }; console.log(`Compiling ${path.relative(process.cwd(), SRC)} ...`); const out = JSON.parse(solc.compile(JSON.stringify(input), { import: findImports })); if (out.errors) { const fatal = out.errors.filter(e => e.severity === 'error'); out.errors.forEach(e => console.log(e.formattedMessage || e.message)); if (fatal.length) { console.error(`\n${fatal.length} error(s) — aborting.`); process.exit(1); } } const contract = out.contracts['BobaiBuyDrops.sol']['BobaiBuyDrops']; const artifact = { abi: contract.abi, bytecode: '0x' + contract.evm.bytecode.object, deployedBytecode: '0x' + contract.evm.deployedBytecode.object, compiler: { version: solc.version() }, }; const outFile = path.join(OUT, 'BobaiBuyDrops.json'); fs.writeFileSync(outFile, JSON.stringify(artifact, null, 2)); const sizeKB = (artifact.bytecode.length / 2 / 1024).toFixed(1); console.log(`OK -> ${path.relative(process.cwd(), outFile)}`); console.log(` bytecode size: ${sizeKB} KB (limit 24 KB for L1; BSC accepts)`); console.log(` abi entries: ${artifact.abi.length}`); ============================================================================== === FILE: nft/contract/scripts/deploy.js ============================================================================== // Deploy BobaiBuyDrops to BNB Chain mainnet via viem. // Reads NFT_RELAYER_PRIVATE_KEY from .env, deploys, appends NFT_CONTRACT_ADDRESS. // // Run: npm run deploy const path = require('path'); const fs = require('fs'); const { createPublicClient, createWalletClient, http, formatEther } = require('viem'); const { privateKeyToAccount } = require('viem/accounts'); const { bsc } = require('viem/chains'); const RPC = process.env.BSC_RPC || 'https://bsc-dataseed.binance.org'; const PK = process.env.NFT_RELAYER_PRIVATE_KEY; if (!PK) { console.error('FATAL: NFT_RELAYER_PRIVATE_KEY missing in .env'); process.exit(1); } const account = privateKeyToAccount(PK); const BASE_URI = process.env.NFT_BASE_URI || ''; // empty placeholder; setBaseURI later const artifact = require(path.join(__dirname, '..', 'build', 'BobaiBuyDrops.json')); (async () => { const publicClient = createPublicClient({ chain: bsc, transport: http(RPC) }); const wallet = createWalletClient({ account, chain: bsc, transport: http(RPC) }); const bal = await publicClient.getBalance({ address: account.address }); console.log(`Deployer/Relayer: ${account.address}`); console.log(`Balance: ${formatEther(bal)} BNB`); if (bal < 5_000_000_000_000_000n) { // < 0.005 BNB console.error('FATAL: balance too low for deploy (need >= 0.005 BNB).'); process.exit(1); } console.log(`\nDeploying BobaiBuyDrops...`); console.log(` constructor baseURI = "${BASE_URI}"`); const hash = await wallet.deployContract({ abi: artifact.abi, bytecode: artifact.bytecode, args: [BASE_URI], }); console.log(`tx hash: ${hash}`); console.log(`Waiting for confirmation...`); const receipt = await publicClient.waitForTransactionReceipt({ hash, confirmations: 2 }); if (receipt.status !== 'success') { console.error('FATAL: deploy tx reverted'); process.exit(1); } const addr = receipt.contractAddress; console.log(`\n✅ Deployed`); console.log(` contract: ${addr}`); console.log(` block: ${receipt.blockNumber}`); console.log(` gas used: ${receipt.gasUsed.toString()}`); console.log(` bscscan: https://bscscan.com/address/${addr}`); const envPath = path.resolve(__dirname, '..', '..', '..', '.env'); fs.appendFileSync(envPath, `\n# Deployed ${new Date().toISOString()}\nNFT_CONTRACT_ADDRESS=${addr}\n`); console.log(`\nNFT_CONTRACT_ADDRESS appended to .env`); })(); ============================================================================== === FILE: nft/contract/scripts/renounce.js ============================================================================== // One-off: renounce ownership of the BobaiBuyDrops contract. // After this runs, owner = 0x0 — setMinter / setBaseURI / setCap are locked forever. // minter address stays set → buy-alerts keep minting normally. // // Run: node scripts/renounce.js require('dotenv').config({ path: require('path').join(__dirname, '..', '..', '..', '.env') }); const { createPublicClient, createWalletClient, http, parseAbi } = require('viem'); const { privateKeyToAccount } = require('viem/accounts'); const { bsc } = require('viem/chains'); const RPC = process.env.BSC_RPC || 'https://bsc-dataseed.binance.org'; const PK = process.env.NFT_RELAYER_PRIVATE_KEY; const ADDR = process.env.NFT_CONTRACT_ADDRESS; if (!PK) { console.error('FATAL: NFT_RELAYER_PRIVATE_KEY missing'); process.exit(1); } if (!ADDR) { console.error('FATAL: NFT_CONTRACT_ADDRESS missing'); process.exit(1); } const ABI = parseAbi([ 'function owner() view returns (address)', 'function minter() view returns (address)', 'function renounceOwnership()', ]); const account = privateKeyToAccount(PK); (async () => { const pub = createPublicClient({ chain: bsc, transport: http(RPC) }); const wallet = createWalletClient({ account, chain: bsc, transport: http(RPC) }); console.log(`Contract: ${ADDR}`); console.log(`Sender: ${account.address}`); const ownerBefore = await pub.readContract({ address: ADDR, abi: ABI, functionName: 'owner' }); const minterBefore = await pub.readContract({ address: ADDR, abi: ABI, functionName: 'minter' }); console.log(`\nBefore:`); console.log(` owner: ${ownerBefore}`); console.log(` minter: ${minterBefore}`); if (ownerBefore.toLowerCase() === '0x0000000000000000000000000000000000000000') { console.log('\nAlready renounced. Nothing to do.'); process.exit(0); } if (ownerBefore.toLowerCase() !== account.address.toLowerCase()) { console.error(`\nFATAL: sender ${account.address} is not owner ${ownerBefore}`); process.exit(1); } console.log(`\nSending renounceOwnership()...`); const hash = await wallet.writeContract({ address: ADDR, abi: ABI, functionName: 'renounceOwnership', args: [], }); console.log(` tx: ${hash}`); console.log(` https://bscscan.com/tx/${hash}`); const r = await pub.waitForTransactionReceipt({ hash, confirmations: 1 }); if (r.status !== 'success') { console.error('Tx reverted'); process.exit(1); } console.log(` ✅ confirmed, gas: ${r.gasUsed.toString()}`); const ownerAfter = await pub.readContract({ address: ADDR, abi: ABI, functionName: 'owner' }); const minterAfter = await pub.readContract({ address: ADDR, abi: ABI, functionName: 'minter' }); console.log(`\nAfter:`); console.log(` owner: ${ownerAfter}`); console.log(` minter: ${minterAfter}`); console.log(`\n${ownerAfter === '0x0000000000000000000000000000000000000000' ? '🔒 RENOUNCED' : '❌ owner did not change'}`); })().catch(e => { console.error(e); process.exit(1); }); ============================================================================== === FILE: nft/contract/scripts/set-base-uri.js ============================================================================== // One-off: set the contract's baseURI to point at the metadata worker. // Run: npm run set-base-uri const path = require('path'); const { createPublicClient, createWalletClient, http, formatEther } = require('viem'); const { privateKeyToAccount } = require('viem/accounts'); const { bsc } = require('viem/chains'); const RPC = process.env.BSC_RPC || 'https://bsc-dataseed.binance.org'; const PK = process.env.NFT_RELAYER_PRIVATE_KEY; const ADDR = process.env.NFT_CONTRACT_ADDRESS; const BASE_URI = process.env.NFT_BASE_URI || 'https://bobai-nft-meta.bobbuildonbnb.workers.dev/meta/'; if (!PK) { console.error('FATAL: NFT_RELAYER_PRIVATE_KEY missing'); process.exit(1); } if (!ADDR) { console.error('FATAL: NFT_CONTRACT_ADDRESS missing'); process.exit(1); } const artifact = require(path.join(__dirname, '..', 'build', 'BobaiBuyDrops.json')); const account = privateKeyToAccount(PK); (async () => { const publicClient = createPublicClient({ chain: bsc, transport: http(RPC) }); const wallet = createWalletClient({ account, chain: bsc, transport: http(RPC) }); console.log(`Owner: ${account.address}`); console.log(`Contract: ${ADDR}`); console.log(`Setting baseURI to: ${BASE_URI}`); const tx = await wallet.writeContract({ address: ADDR, abi: artifact.abi, functionName: 'setBaseURI', args: [BASE_URI], }); console.log(`tx: ${tx}`); const r = await publicClient.waitForTransactionReceipt({ hash: tx, confirmations: 1 }); console.log(r.status === 'success' ? '✅ baseURI updated' : '❌ tx reverted'); console.log(`gas: ${r.gasUsed.toString()}`); })(); ============================================================================== === FILE: nft/contract/scripts/setup.js ============================================================================== // Post-deploy setup: sets the 6 per-tier caps via setCapBatch + sets minter. // Run AFTER deploy.js. Reads NFT_CONTRACT_ADDRESS + NFT_RELAYER_PRIVATE_KEY from .env. // // Run: npm run setup const path = require('path'); const { createPublicClient, createWalletClient, http, formatEther } = require('viem'); const { privateKeyToAccount } = require('viem/accounts'); const { bsc } = require('viem/chains'); const RPC = process.env.BSC_RPC || 'https://bsc-dataseed.binance.org'; const PK = process.env.NFT_RELAYER_PRIVATE_KEY; const ADDR = process.env.NFT_CONTRACT_ADDRESS; const MINTER = process.env.NFT_MINTER_ADDRESS || process.env.NFT_RELAYER_ADDRESS; if (!PK) { console.error('FATAL: NFT_RELAYER_PRIVATE_KEY missing'); process.exit(1); } if (!ADDR) { console.error('FATAL: NFT_CONTRACT_ADDRESS missing — run deploy first'); process.exit(1); } if (!MINTER) { console.error('FATAL: minter address missing'); process.exit(1); } const artifact = require(path.join(__dirname, '..', 'build', 'BobaiBuyDrops.json')); const account = privateKeyToAccount(PK); // Per-tier caps. Sum = 1925. // NICE BIG HUGE WHALE THUNDER KRAKEN const CAPS = [1000, 500, 250, 100, 50, 25]; (async () => { const publicClient = createPublicClient({ chain: bsc, transport: http(RPC) }); const wallet = createWalletClient({ account, chain: bsc, transport: http(RPC) }); console.log(`Owner/Sender: ${account.address}`); console.log(`Contract: ${ADDR}`); console.log(`Minter target: ${MINTER}`); const bal = await publicClient.getBalance({ address: account.address }); console.log(`Balance: ${formatEther(bal)} BNB`); const tiers = [0, 1, 2, 3, 4, 5]; const total = CAPS.reduce((a, b) => a + b, 0); console.log(`Total NFTs: ${total} (expected 1925)`); if (total !== 1925) { console.error('Cap sum mismatch — aborting'); process.exit(1); } // ----- 1) setCapBatch console.log('\n[1/2] setCapBatch(6 tiers)...'); const txCap = await wallet.writeContract({ address: ADDR, abi: artifact.abi, functionName: 'setCapBatch', args: [tiers, CAPS.map(n => BigInt(n))], }); console.log(` tx: ${txCap}`); const rcap = await publicClient.waitForTransactionReceipt({ hash: txCap, confirmations: 1 }); if (rcap.status !== 'success') { console.error('setCapBatch reverted'); process.exit(1); } console.log(` ✅ confirmed, gas: ${rcap.gasUsed.toString()}`); // ----- 2) setMinter console.log('\n[2/2] setMinter()...'); const txMin = await wallet.writeContract({ address: ADDR, abi: artifact.abi, functionName: 'setMinter', args: [MINTER], }); console.log(` tx: ${txMin}`); const rmin = await publicClient.waitForTransactionReceipt({ hash: txMin, confirmations: 1 }); if (rmin.status !== 'success') { console.error('setMinter reverted'); process.exit(1); } console.log(` ✅ confirmed, gas: ${rmin.gasUsed.toString()}`); // ----- verify const [mintedArr, capArr] = await publicClient.readContract({ address: ADDR, abi: artifact.abi, functionName: 'getTiers', }); const minter = await publicClient.readContract({ address: ADDR, abi: artifact.abi, functionName: 'minter', }); const sumCap = capArr.reduce((a, b) => a + b, 0n); const sumMinted = mintedArr.reduce((a, b) => a + b, 0n); const LABEL = ['NICE','BIG','HUGE','WHALE','THUNDER','KRAKEN']; console.log('\n=== Verification ==='); console.log(`minter: ${minter}`); console.log(`per-tier caps: ${capArr.map((c, i) => `${LABEL[i]}=${c}`).join(', ')}`); console.log(`total cap: ${sumCap} (expect 1925)`); console.log(`minted: ${sumMinted}`); console.log(`\nBscScan: https://bscscan.com/address/${ADDR}`); })(); ============================================================================== === FILE: nft/contract/src/BobaiBuyDrops.sol ============================================================================== // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; /// @title BOBAI Buy Drops /// @notice Auto-minted NFTs awarded for qualifying $BOBAI buys on BNB Chain. /// Each NFT carries a buy-tier (motif, fixed by purchase USD value) and /// a rarity (color, rolled at mint). Per-tier hard caps sum to 1925 total. contract BobaiBuyDrops is ERC721, Ownable { using Strings for uint256; // tier: 0=NICE 1=BIG 2=HUGE 3=WHALE 4=THUNDER 5=KRAKEN // rarity: 0=Common 1=Uncommon 2=Rare 3=Mythical 4=Legendary 5=Ancient 6=Immortal address public minter; string private _base; uint256 public nextId = 1; mapping(uint256 => uint8) public tierOf; mapping(uint256 => uint8) public rarityOf; mapping(uint8 => uint256) public minted; // tier => count mapping(uint8 => uint256) public cap; // tier => max event BuyDrop(address indexed to, uint256 indexed tokenId, uint8 tier, uint8 rarity); event MinterUpdated(address indexed minter); constructor(string memory baseURI_) ERC721("BOBAI Buy Drops", "BOBAIBUY") Ownable(msg.sender) { _base = baseURI_; } modifier onlyMinter() { require(msg.sender == minter, "not minter"); _; } // ---------- admin ---------- function setMinter(address m) external onlyOwner { minter = m; emit MinterUpdated(m); } function setBaseURI(string calldata u) external onlyOwner { _base = u; } function setCap(uint8 tier, uint256 max_) external onlyOwner { require(tier <= 5, "bad tier"); cap[tier] = max_; } function setCapBatch(uint8[] calldata tiers, uint256[] calldata maxes) external onlyOwner { require(tiers.length == maxes.length, "len mismatch"); for (uint256 i; i < tiers.length; ++i) { require(tiers[i] <= 5, "bad tier"); cap[tiers[i]] = maxes[i]; } } // ---------- mint ---------- function mintTo(address to, uint8 tier, uint8 rarity) external onlyMinter returns (uint256 id) { require(tier <= 5, "bad tier"); require(rarity <= 6, "bad rarity"); require(minted[tier] < cap[tier], "tier sold out"); id = nextId++; minted[tier] += 1; tierOf[id] = tier; rarityOf[id] = rarity; // _mint (not _safeMint): contract wallets without onERC721Received would // revert _safeMint and the buyer would lose their drop. _mint(to, id); emit BuyDrop(to, id, tier, rarity); } // ---------- views ---------- function tokenURI(uint256 id) public view override returns (string memory) { _requireOwned(id); return string(abi.encodePacked(_base, id.toString(), ".json")); } /// Returns minted + cap for all 6 tiers in one call (cheap dashboard read). function getTiers() external view returns (uint256[6] memory mintedArr, uint256[6] memory capArr) { for (uint8 t = 0; t < 6; ++t) { mintedArr[t] = minted[t]; capArr[t] = cap[t]; } } } ============================================================================== === FILE: worker-nft-meta/index.js ============================================================================== // BOBAI NFT Metadata Worker — Cloudflare Worker // Serves OpenSea-spec JSON metadata per token: GET /meta/(.json) // Looks up tier/rarity on-chain and returns image URL + traits. import { createPublicClient, http, fallback, parseAbi } from 'viem'; import { bsc } from 'viem/chains'; const NFT_ABI = parseAbi([ 'function tierOf(uint256) view returns (uint8)', 'function rarityOf(uint256) view returns (uint8)', 'function nextId() view returns (uint256)', ]); const RPC_DATASEED = [ 'https://bsc-dataseed1.binance.org', 'https://bsc-dataseed2.binance.org', 'https://bsc-dataseed3.binance.org', ]; const TIER_INFO = [ { slug: 'nice-buy', label: 'NICE BUY', threshold: '$100+', emoji: '💰' }, { slug: 'big-buy', label: 'BIG BUY', threshold: '$150+', emoji: '💎' }, { slug: 'huge-buy', label: 'HUGE BUY', threshold: '$250+', emoji: '🚀' }, { slug: 'whale-buy', label: 'WHALE BUY', threshold: '$500+', emoji: '🐋' }, { slug: 'thunder-buy', label: 'THUNDER BUY', threshold: '$1000+', emoji: '⚡' }, { slug: 'kraken-buy', label: 'KRAKEN BUY', threshold: '$2500+', emoji: '🦑' }, ]; const RARITY_INFO = [ { slug: 'common', label: 'Common', hex: '#b0c3d9' }, { slug: 'uncommon', label: 'Uncommon', hex: '#5e98d9' }, { slug: 'rare', label: 'Rare', hex: '#4b69ff' }, { slug: 'mythical', label: 'Mythical', hex: '#8847ff' }, { slug: 'legendary', label: 'Legendary', hex: '#d32ce6' }, { slug: 'ancient', label: 'Ancient', hex: '#eb4b4b' }, { slug: 'immortal', label: 'Immortal', hex: '#b28a33' }, ]; const COLLECTION_NAME = 'BOBAI Buy Drops'; const COLLECTION_DESC = 'Auto-minted NFTs awarded for qualifying $BOBAI buys on BNB Chain. ' + 'Every buy of $100 or more earns an NFT with a fixed buy-tier motif and ' + 'a rarity drawn from the drop matrix. Collection capped at 1,925 NFTs.'; const EXTERNAL_URL = 'https://brainonbnb.com'; function json(body, opts = {}) { return new Response(JSON.stringify(body), { status: opts.status || 200, headers: { 'Content-Type': 'application/json; charset=utf-8', // Short cache so wallet/marketplace indexers (Element, Trust, MM) refresh // quickly after a contract metadata change. Image URL itself is long-cached // by the dashboard origin, so the marginal cost of a 30s JSON fetch is tiny. 'Cache-Control': opts.cache || 'public, max-age=30, s-maxage=30', 'Access-Control-Allow-Origin': '*', }, }); } // 42 card filenames live both at brainonbnb.com (primary) and in the Pinata pin // (IPFS fallback). The /img route below proxies primary→IPFS so the public // image URL on each NFT is independent of brainonbnb.com being reachable. const ALLOWED_TIERS = new Set(['nice-buy','big-buy','huge-buy','whale-buy','thunder-buy','kraken-buy']); const ALLOWED_RARS = new Set(['common','uncommon','rare','mythical','legendary','ancient','immortal']); const PINATA_CID = 'bafybeibc7nydc4tgk5lrccmvneyfdllnlt5hveaml6lfocj5u4ivw4xiae'; async function fetchImageWithFallback(file) { const primary = `https://brainonbnb.com/nft/cards/${file}`; const fallback = `https://${PINATA_CID}.ipfs.dweb.link/${file}`; try { const r = await fetch(primary, { cf: { cacheTtl: 86400, cacheEverything: true }, signal: AbortSignal.timeout(6000), }); if (r.ok) { return new Response(r.body, { status: 200, headers: { 'Content-Type': 'image/jpeg', 'Cache-Control': 'public, max-age=14400, s-maxage=86400', 'Access-Control-Allow-Origin': '*', 'X-Image-Source': 'origin', }, }); } } catch (_e) { /* fall through to IPFS */ } const r = await fetch(fallback, { cf: { cacheTtl: 86400, cacheEverything: true } }); return new Response(r.body, { status: r.status, headers: { 'Content-Type': 'image/jpeg', 'Cache-Control': r.ok ? 'public, max-age=14400, s-maxage=86400' : 'public, max-age=30', 'Access-Control-Allow-Origin': '*', 'X-Image-Source': 'ipfs', }, }); } export default { async fetch(request, env, ctx) { const url = new URL(request.url); const path = url.pathname; // GET /img/-.jpg — origin→IPFS failover proxy. // Wallets call this URL; if brainonbnb.com is down/lapsed/blocked, the // worker silently serves the same bytes from the Pinata IPFS pin. const mImg = path.match(/^\/img\/([a-z]+-buy)-([a-z]+)\.jpg$/i); if (mImg) { const tier = mImg[1].toLowerCase(); const rar = mImg[2].toLowerCase(); if (!ALLOWED_TIERS.has(tier) || !ALLOWED_RARS.has(rar)) { return new Response('Not found', { status: 404 }); } return fetchImageWithFallback(`${tier}-${rar}.jpg`); } // GET /meta/ or /meta/.json const m = path.match(/^\/meta\/(\d+)(?:\.json)?$/); if (!m) { if (path === '/' || path === '/health') { return json({ name: COLLECTION_NAME, status: 'ok' }); } return new Response('Not found', { status: 404 }); } const tokenId = BigInt(m[1]); if (tokenId === 0n) { return json({ error: 'token id must be >= 1' }, { status: 400 }); } // Fallback across all dataseeds — a single-RPC hiccup must not surface as a // 502 to wallet/marketplace indexers (they cache failures aggressively). const client = createPublicClient({ chain: bsc, transport: fallback(RPC_DATASEED.map((u) => http(u, { timeout: 5000 }))), }); const contract = env.NFT_CONTRACT_ADDRESS; // Resolve tier/rarity from chain. Fail soft if token not yet minted. let tier, rarity, nextId; try { [tier, rarity, nextId] = await Promise.all([ client.readContract({ address: contract, abi: NFT_ABI, functionName: 'tierOf', args: [tokenId] }), client.readContract({ address: contract, abi: NFT_ABI, functionName: 'rarityOf', args: [tokenId] }), client.readContract({ address: contract, abi: NFT_ABI, functionName: 'nextId' }), ]); } catch (e) { return json({ error: 'chain read failed', detail: e.shortMessage || e.message }, { status: 502 }); } if (tokenId >= nextId) { return json({ error: 'token not minted yet' }, { status: 404 }); } const tierIdx = Number(tier); const rarIdx = Number(rarity); if (tierIdx > 5 || rarIdx > 6) { return json({ error: 'bad tier or rarity from chain', tier: tierIdx, rarity: rarIdx }, { status: 500 }); } const tInfo = TIER_INFO[tierIdx]; const rInfo = RARITY_INFO[rarIdx]; // image: points at this worker's own /img route, which transparently proxies // brainonbnb.com first and falls back to the Pinata IPFS pin on origin failure. // Keeps wallet-facing URL stable forever as long as this CF account lives — // domain renewal, origin outage, even a full brainonbnb.com loss are absorbed. const cardsBase = (env.CARDS_BASE_URL || `${url.origin}/img`).replace(/\/$/, ''); const meta = { name: `${COLLECTION_NAME} #${tokenId}`, description: `${COLLECTION_DESC}\n\n` + `This piece: ${tInfo.emoji} ${tInfo.label} motif (${tInfo.threshold}) ` + `in ${rInfo.label} rarity (${rInfo.hex}).`, image: `${cardsBase}/${tInfo.slug}-${rInfo.slug}.jpg`, // `image_url` is an older alias some indexers (Bitget/DeBank-Rabby etc.) // read instead of `image`. Same URL — kept in sync — for max wallet reach. image_url: `${cardsBase}/${tInfo.slug}-${rInfo.slug}.jpg`, external_url: EXTERNAL_URL, background_color: rInfo.hex.replace('#', ''), attributes: [ { trait_type: 'Buy Tier', value: tInfo.label }, { trait_type: 'Tier Threshold', value: tInfo.threshold }, { trait_type: 'Rarity', value: rInfo.label }, { trait_type: 'Rarity Color', value: rInfo.hex }, { trait_type: 'Serial', display_type: 'number', value: Number(tokenId) }, { trait_type: 'Collection Size', display_type: 'number', value: 1925 }, ], }; return json(meta); }, }; ============================================================================== === FILE: worker-nft-meta/wrangler.toml ============================================================================== name = "bobai-nft-meta" main = "index.js" compatibility_date = "2024-09-23" compatibility_flags = ["nodejs_compat"] [vars] NFT_CONTRACT_ADDRESS = "0xd56226b3b8297a57f4361fca28aa43babdc9789d" # CARDS_BASE_URL intentionally unset: defaults to this worker's own /img route, # which proxies brainonbnb.com → Pinata IPFS with automatic failover. # Override only if you ever want to bypass the failover and serve a fixed origin.