MemeID Developer API

Integrate MemeID domains into your Solana app.

Overview

MemeID integrates directly with the Solana program through RPC. There is no hosted REST API: you read and write domains with @solana/web3.js or Anchor from any Node.js script or backend. Every domain is a program-derived account holding its name and owner, so reads need no API key and no wallet. Registering needs the owner to sign one transaction.

PropertyValue
NetworkSolana Mainnet
Program IDFgWNULA55FtxhE6dXuk7tu6E7isk92nC318CB29qLNqK
RPChttps://api.mainnet-beta.solana.com
Registration feeFree — the owner only pays account rent and the network fee
Expiry / renewalsNone. Domains are held permanently

Supported domains

Each domain uses one of seven suffixes. Any other suffix is rejected by the program.

.meme.pump.moon.fun.bull.bear.degen

Installation

The examples are plain Node.js with TypeScript (ES modules, Node 18+). They use the v1 @solana/web3.js API, bs58 for account filters, and Anchor 0.32.1 for the optional Anchor client.

terminal
npm install @coral-xyz/anchor@0.32.1 @solana/web3.js@1 bs58

Configuration

Create one connection and the Program ID, then share them across the helpers below.

config.ts
import { Connection, PublicKey } from "@solana/web3.js";

const RPC_URL = "https://api.mainnet-beta.solana.com";

export const connection = new Connection(RPC_URL, "confirmed");

export const MEMEID_PROGRAM_ID = new PublicKey(
  "FgWNULA55FtxhE6dXuk7tu6E7isk92nC318CB29qLNqK"
);
constants.ts
export const NAMESPACES = ["meme", "pump", "moon", "fun", "bull", "bear", "degen"] as const;
export type Namespace = (typeof NAMESPACES)[number];

// Anchor discriminators for the register instruction and NameRecord account
export const REGISTER_DISCRIMINATOR = Buffer.from([211, 124, 67, 15, 211, 194, 178, 240]);
export const NAME_RECORD_DISCRIMINATOR = Buffer.from([254, 22, 17, 161, 229, 49, 238, 105]);

// 8 discriminator + 4 string length + 32 name bytes + 32 owner bytes
export const NAME_RECORD_SPACE = 76;

Program ID

MemeID runs on Solana Mainnet at the Program ID below. Reuse it for PDA derivation, instructions and account queries.

Mainnet Program ID
FgWNULA55FtxhE6dXuk7tu6E7isk92nC318CB29qLNqK
View program on Solscan

RPC

The examples use the public endpoint https://api.mainnet-beta.solana.com. It is rate-limited, so use a dedicated Mainnet RPC provider for production traffic. All examples use confirmed commitment.

Domain format

A domain is name.suffix. Names are 1–32 ASCII letters or digits and are stored lowercase, so Frog.meme and frog.meme are the same domain. Always normalize before deriving an address.

parse.ts
const LABEL_PATTERN = /^[a-z0-9]{1,32}$/;

export function parseDomain(input: string) {
  const parts = input.trim().toLowerCase().split(".");
  if (parts.length !== 2) throw new Error("Use the format name.suffix, e.g. frog.meme");

  const [name, namespace] = parts;
  if (!LABEL_PATTERN.test(name)) {
    throw new Error("Names use 1-32 letters or digits only");
  }
  if (!(NAMESPACES as readonly string[]).includes(namespace)) {
    throw new Error("Unsupported suffix");
  }
  return { name, namespace: namespace as Namespace, fullName: name + "." + namespace };
}

PDA derivation

Each domain lives at a PDA seeded with "name", the suffix and the name. The suffix is only part of the seed — it is not stored in the account.

pda.ts
import { PublicKey } from "@solana/web3.js";

export function deriveDomainPda(name: string, namespace: Namespace): PublicKey {
  return PublicKey.findProgramAddressSync(
    [Buffer.from("name"), Buffer.from(namespace), Buffer.from(name)],
    MEMEID_PROGRAM_ID
  )[0];
}

const pda = deriveDomainPda("frog", "meme");
console.log(pda.toBase58());

Check availability

A domain is available when no name record exists at its PDA. isDomainAvailable builds on getDomainOwner from Get owner.

availability.ts
export async function isDomainAvailable(domain: string) {
  return (await getDomainOwner(domain)) === null;
}

console.log(await isDomainAvailable("frog.meme"));

Availability is a snapshot, not a reservation — another wallet can register the name before your transaction lands. Treat RPC errors as errors, never as “available”.

Register domain

register(name, namespace) creates the name record and sets the signer as owner. The owner pays rent for the 76-byte account; query getMinimumBalanceForRentExemption(76) instead of hardcoding it.

AccountSignerWritableNotes
nameRecordNoYesDomain PDA, created by the instruction
ownerYesYesPays rent and becomes the owner
systemProgramNoNo11111111111111111111111111111111
register.ts
import { readFileSync } from "node:fs";
import {
  Keypair,
  PublicKey,
  SystemProgram,
  Transaction,
  TransactionInstruction,
  sendAndConfirmTransaction,
} from "@solana/web3.js";

function borshString(value: string) {
  const bytes = Buffer.from(value, "utf8");
  const length = Buffer.alloc(4);
  length.writeUInt32LE(bytes.length, 0);
  return Buffer.concat([length, bytes]);
}

// register(name: String, namespace: String)
export function buildRegisterInstruction(name: string, namespace: Namespace, owner: PublicKey) {
  return new TransactionInstruction({
    programId: MEMEID_PROGRAM_ID,
    keys: [
      { pubkey: deriveDomainPda(name, namespace), isSigner: false, isWritable: true }, // nameRecord
      { pubkey: owner, isSigner: true, isWritable: true },                              // owner
      { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },          // systemProgram
    ],
    data: Buffer.concat([REGISTER_DISCRIMINATOR, borshString(name), borshString(namespace)]),
  });
}

export async function registerDomain(domain: string, owner: Keypair) {
  const { name, namespace } = parseDomain(domain);
  const tx = new Transaction().add(buildRegisterInstruction(name, namespace, owner.publicKey));

  try {
    return await sendAndConfirmTransaction(connection, tx, [owner], { commitment: "confirmed" });
  } catch (error) {
    // Confirmation can expire even when the transaction landed. The chain is the source of truth.
    const current = await getDomainOwner(domain);
    if (current?.equals(owner.publicKey)) return null;
    throw error;
  }
}

// Load the owner keypair from a Solana CLI keypair file
const owner = Keypair.fromSecretKey(
  Uint8Array.from(JSON.parse(readFileSync("./owner.json", "utf8")))
);

Prefer Anchor? Use the production IDL below with Anchor 0.32.1 and the same owner keypair. It is not fetched from chain, so Program.fetchIdl() is not needed.

anchor.ts
import { AnchorProvider, Program, Wallet, type Idl } from "@coral-xyz/anchor";
import { SystemProgram } from "@solana/web3.js";

// MemeID client IDL (Anchor 0.32.1 format)
export const MEMEID_IDL: Idl = {
  address: "FgWNULA55FtxhE6dXuk7tu6E7isk92nC318CB29qLNqK",
  metadata: { name: "meme_names", version: "0.1.0", spec: "0.1.0" },
  instructions: [{
    name: "register",
    discriminator: [211, 124, 67, 15, 211, 194, 178, 240],
    accounts: [
      { name: "nameRecord", writable: true },
      { name: "owner", writable: true, signer: true },
      { name: "systemProgram", address: "11111111111111111111111111111111" },
    ],
    args: [
      { name: "name", type: "string" },
      { name: "namespace", type: "string" },
    ],
  }],
  accounts: [{ name: "nameRecord", discriminator: [254, 22, 17, 161, 229, 49, 238, 105] }],
  types: [{
    name: "nameRecord",
    type: { kind: "struct", fields: [{ name: "name", type: "string" }, { name: "owner", type: "pubkey" }] },
  }],
};

const provider = new AnchorProvider(connection, new Wallet(owner), { commitment: "confirmed" });
const program = new Program(MEMEID_IDL, provider);

const signature = await program.methods
  .register("frog", "meme")
  .accountsStrict({
    nameRecord: deriveDomainPda("frog", "meme"),
    owner: owner.publicKey,
    systemProgram: SystemProgram.programId,
  })
  .rpc();

Get owner

Fetch the PDA and decode it. The account layout is NameRecord { name: String, owner: Pubkey }, Borsh-encoded after an 8-byte discriminator (76 bytes total).

decode.ts
import { PublicKey } from "@solana/web3.js";

// NameRecord { name: String, owner: Pubkey }, Borsh encoded after the 8-byte discriminator
export function decodeNameRecord(data: Buffer) {
  if (!data.subarray(0, 8).equals(NAME_RECORD_DISCRIMINATOR)) {
    throw new Error("Not a MemeID name record");
  }
  const length = data.readUInt32LE(8);
  const name = data.subarray(12, 12 + length).toString("utf8");
  const owner = new PublicKey(data.subarray(12 + length, 12 + length + 32));
  return { name, owner };
}
lookup.ts
import { PublicKey, SystemProgram } from "@solana/web3.js";

export async function getDomainOwner(domain: string): Promise<PublicKey | null> {
  const { name, namespace } = parseDomain(domain);
  const pda = deriveDomainPda(name, namespace);
  const info = await connection.getAccountInfo(pda, "confirmed");

  // A PDA can be prefunded with lamports without ever being registered.
  if (!info || (info.owner.equals(SystemProgram.programId) && info.data.length === 0)) {
    return null;
  }
  if (!info.owner.equals(MEMEID_PROGRAM_ID)) throw new Error("Unexpected account at domain PDA");

  const record = decodeNameRecord(info.data);
  if (record.name !== name) throw new Error("Stored name does not match the requested domain");
  return record.owner;
}

const owner = await getDomainOwner("frog.meme");
console.log(owner?.toBase58() ?? "Not registered");

Fetch domains owned by wallet

Filter program accounts by the name record discriminator, then match owners. Because the suffix is not stored, recover it by re-deriving the PDA for each suffix.

owned.ts
import bs58 from "bs58";
import { PublicKey } from "@solana/web3.js";

export async function getDomainsOwnedBy(owner: PublicKey) {
  const accounts = await connection.getProgramAccounts(MEMEID_PROGRAM_ID, {
    commitment: "confirmed",
    filters: [{ memcmp: { offset: 0, bytes: bs58.encode(NAME_RECORD_DISCRIMINATOR) } }],
  });

  return accounts.flatMap(({ pubkey, account }) => {
    const record = decodeNameRecord(account.data);
    if (!record.owner.equals(owner)) return [];
    // The suffix is not stored; recover it by matching the PDA for each namespace.
    const namespace = NAMESPACES.find((ns) => deriveDomainPda(record.name, ns).equals(pubkey));
    return namespace ? [{ fullName: record.name + "." + namespace, pda: pubkey.toBase58() }] : [];
  });
}

Recent registrations

Registration times are not stored in the account. To build a feed, walk the program’s transaction history and decode register instructions.

recent.ts
const signatures = await connection.getSignaturesForAddress(MEMEID_PROGRAM_ID, { limit: 40 });

for (const { signature, err } of signatures) {
  if (err) continue;
  const tx = await connection.getTransaction(signature, {
    commitment: "confirmed",
    maxSupportedTransactionVersion: 0,
  });
  // For each instruction sent to MEMEID_PROGRAM_ID whose data starts with REGISTER_DISCRIMINATOR:
  //   args     = two Borsh strings (name, namespace) after the discriminator
  //   accounts = [nameRecord, owner, systemProgram]
}

Many RPC providers limit batched transaction requests. Fetch one transaction at a time with limited concurrency and cache the result.

Error handling

SituationWhat to do
already in use on registerThe domain was taken first. Show it as registered.
block height exceeded / confirmation timeoutThe transaction may still have landed. Re-read the PDA before reporting failure.
Wallet rejects the requestTreat as a user cancel, not an error.
PDA holds lamports but no dataSomeone prefunded the address. The domain is still available.
Account owned by another programUnexpected — surface an error and do not register.

Quick start

With the helpers above in your project, checking and registering a domain looks like this:

quick-start.ts
const domain = "frog.meme";

if (await isDomainAvailable(domain)) {
  const signature = await registerDomain(domain, owner);
  console.log("Registered", domain, signature);
} else {
  console.log(domain, "is owned by", (await getDomainOwner(domain))?.toBase58());
}