bull name service

Integrate .bull names

Every .bull name is a Metaplex Core NFT minted by a Solana program — whoever holds the NFT owns the name. Names never expire and can never be rewritten. Resolution works three ways: a CORS-open HTTP API, a tiny TypeScript SDK, or raw on-chain reads.

Addresses

Program (mainnet)2HQrqBW7wNNhcWuHb6Ub4dUkgEZ6pgwM8PYCXn38Tfj9
Program (devnet)BwLd96v6kAyjqgNnCQpWCp93yrDihqkAfbXyfgYCBh8h
Treasury (Squads vault)BiVV9xiAyZfQqbbU7GmRWAxhXJsyrz3qempfXaLahYv8
Metaplex CoreCoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d

HTTP API

CORS-open, edge-cached, no key required. Ownership is read live from the chain, so transfers and sales reflect within seconds.

GET https://www.bns.wtf/api/resolve/<name>

# registered
{
  "name": "crispy.bull",
  "registered": true,
  "owner": "8wvq...Wteb",        // controlling wallet
  "asset": "FPrW...HVzx",        // the Core NFT
  "registeredAt": "2026-08-18T09:12:00.000Z",
  "image": "https://img.bns.wtf/domains/crispy-1fe752c2.jpg",
  "metadata": "https://www.bns.wtf/api/meta/crispy",
  "listing": { "priceLamports": 500000000, "seller": "...", "listedAt": "..." } | null
}

# unregistered
{ "name": "free.bull", "registered": false, "registerPriceLamports": 200000000 }
GET https://www.bns.wtf/api/wallet/<address>   # reverse lookup

{
  "address": "8wvq...Wteb",
  "count": 2,
  "domains": [
    { "name": "crispy.bull", "asset": "FPrW...", "image": "https://...", "listed": false },
    { "name": "moon.bull", "asset": "9aBc...", "image": "https://...", "listed": true, "priceLamports": 1000000000 }
  ]
}
GET https://www.bns.wtf/api/directory   # every domain + listings + recent sales
GET https://www.bns.wtf/api/meta/<name> # NFT metadata (name, image, attributes)

TypeScript SDK

npm install bull-name-service @solana/web3.js
import { Connection } from "@solana/web3.js";
import { resolve, getDomainState } from "bull-name-service";

const connection = new Connection("https://api.mainnet-beta.solana.com");

const owner = await resolve(connection, "crispy.bull");
// PublicKey | null — if listed for sale, this is the seller

const state = await getDomainState(connection, "crispy");
// { name, owner, asset, registeredAt, imageUri, listing }

Also exported: getDomainKey, getDomainsByOwner, validateName, and DEVNET_PROGRAM_ID (pass { programId: DEVNET_PROGRAM_ID } to target devnet).

On-chain resolution (no dependencies)

A domain record is a PDA of the program with seeds ["domain", name] (name lowercase, no suffix). Account layout:

offset  size      field
0       8         discriminator [130,61,36,125,56,48,189,247]
8       4         name length (u32 LE)
12      n         name (utf-8)
12+n    32        asset pubkey (the Core NFT)
44+n    8         registered_at (i64 LE, unix seconds)
52+n    1         bump
53+n    4 + m     image_uri (u32 LE length + utf-8; empty = default art)

The current holder is inside the Core asset account: bytes [1..33] are the owner pubkey. If the domain is listed on the native market the asset is held by the escrow PDA (seeds ["escrow"]); the listing PDA (seeds ["listing", name]) then records the real seller and price. Valid names: 1–32 chars of a-z 0-9 -, no leading/trailing hyphen.

Fees

Registration (one-time, forever): 5 SOL for 1–2 chars, 2 SOL for 3, 0.5 SOL for 4, 0.2 SOL for 5+. Native marketplace sales pay 90% to the seller, 10% to the treasury. Secondary sales anywhere carry a 5% royalty enforced by the Core Royalties plugin. Program upgrade authority and the treasury are a Squads 2-of-3 multisig.

Questions or integrating .bull somewhere? bns.wtf