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}