# 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
$100+ · 1000 max
💎
BIG BUY
$150+ · 500 max
🚀
HUGE BUY
$250+ · 250 max
🐋
WHALE BUY
$500+ · 100 max
⚡
THUNDER BUY
$1000+ · 50 max
🦑
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.
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.
#
Tier
Rarity
Recipient
Time
Tx
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.