packages/core/src/collections.ts
The checks a collection must pass. Shown whole, as it was in the repository when this site was built. Line numbers link: add #L12 to the address.
packages/core/src/collections.ts79 lines
1// Which NFT collections can be paired with a coin. Nobody registers a collection by hand: the keeper reads market2// data and the chain, and a collection is listed only when every rule below passes. The checks rerun daily.3 4/** At most this many NFTs, so every owner can be read and paid each run. */5export const MAX_COLLECTION_SIZE = 20_000;6/** At least this many, so rarity tiers mean something. */7export const MIN_COLLECTION_SIZE = 100;8/** No new NFTs minted for this long: the rarity list is final. */9export const MINT_QUIET_DAYS = 7;10 11/** Bait words that mark scam or dead collections. */12const BAIT_RE = /\b(claim|reward|rewards|airdrop|retired|do not buy|dont buy|don't buy|voucher|giveaway|free mint|whitelist|wl pass|access pass)\b|https?:\/\/|www\.|\.(com|io|xyz|fun|net|org)\b/i;13 14export function baitName(name: string): boolean {15 return BAIT_RE.test(name);16}17 18export type CollectionCheck =19 | 'verified'20 | 'traded'21 | 'standard'22 | 'clean_name'23 | 'size'24 | 'mint_finished';25 26export interface CollectionCheckInput {27 name: string;28 /** the collection is on a verified list (Magic Eden) and its NFTs carry the verified collection key on-chain */29 verified: boolean;30 /** floor price in lamports */31 floorLamports: bigint;32 /** lifetime volume in lamports */33 volumeAllLamports: bigint;34 /** every NFT is a regular (uncompressed) Metaplex NFT or a Metaplex Core asset */35 standard: boolean;36 size: number;37 /** newest mint time (unix ms), when known */38 lastMintAt: number | null;39 now?: number;40}41 42export interface CollectionCheckResult {43 ok: boolean;44 failed: CollectionCheck[];45}46 47export const CHECK_LABELS: Record<CollectionCheck, { title: string; detail: string }> = {48 verified: { title: 'Verified', detail: 'Listed as verified on Magic Eden, and every NFT carries the verified collection key on-chain.' },49 traded: { title: 'Actively traded', detail: 'A floor price above zero and some trading volume.' },50 standard: { title: 'A standard Solana NFT', detail: 'Metaplex NFTs (regular or programmable) or Metaplex Core assets, one owner per NFT. Compressed NFTs are not supported yet.' },51 clean_name: { title: 'Clean name', detail: 'No links or bait words such as “claim”, “reward”, “airdrop” or “do not buy”.' },52 size: { title: 'Reasonable size', detail: `Between ${MIN_COLLECTION_SIZE} and ${MAX_COLLECTION_SIZE.toLocaleString('en-US')} NFTs, so every owner can be read and paid.` },53 mint_finished: { title: 'Minting finished', detail: `No new NFTs in the last ${MINT_QUIET_DAYS} days, so the rarity list is final.` },54};55 56export function checkCollection(c: CollectionCheckInput): CollectionCheckResult {57 const failed: CollectionCheck[] = [];58 const now = c.now ?? Date.now();59 if (!c.verified) failed.push('verified');60 if (c.floorLamports <= 0n || c.volumeAllLamports <= 0n) failed.push('traded');61 if (!c.standard) failed.push('standard');62 if (baitName(c.name)) failed.push('clean_name');63 if (c.size < MIN_COLLECTION_SIZE || c.size > MAX_COLLECTION_SIZE) failed.push('size');64 if (c.lastMintAt !== null && now - c.lastMintAt < MINT_QUIET_DAYS * 86_400_000) failed.push('mint_finished');65 return { ok: failed.length === 0, failed };66}67 68/** URL-safe slug from a collection name ("Mad Lads" → "mad-lads"). */69export function slugify(name: string): string {70 return (71 name72 .toLowerCase()73 .normalize('NFKD')74 .replace(/[̀-ͯ]/g, '')75 .replace(/[^a-z0-9]+/g, '-')76 .replace(/^-+|-+$/g, '')77 .slice(0, 48) || 'collection'78 );79}