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.
| Property | Value |
|---|---|
| Network | Solana Mainnet |
| Program ID | FgWNULA55FtxhE6dXuk7tu6E7isk92nC318CB29qLNqK |
| RPC | https://api.mainnet-beta.solana.com |
| Registration fee | Free — the owner only pays account rent and the network fee |
| Expiry / renewals | None. 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.degenInstallation
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.
npm install @coral-xyz/anchor@0.32.1 @solana/web3.js@1 bs58Configuration
Create one connection and the Program ID, then share them across the helpers below.
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"
);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.
FgWNULA55FtxhE6dXuk7tu6E7isk92nC318CB29qLNqKRPC
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.
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.
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.
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.
| Account | Signer | Writable | Notes |
|---|---|---|---|
nameRecord | No | Yes | Domain PDA, created by the instruction |
owner | Yes | Yes | Pays rent and becomes the owner |
systemProgram | No | No | 11111111111111111111111111111111 |
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.
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).
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 };
}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.
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.
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
| Situation | What to do |
|---|---|
already in use on register | The domain was taken first. Show it as registered. |
block height exceeded / confirmation timeout | The transaction may still have landed. Re-read the PDA before reporting failure. |
| Wallet rejects the request | Treat as a user cancel, not an error. |
| PDA holds lamports but no data | Someone prefunded the address. The domain is still available. |
| Account owned by another program | Unexpected — surface an error and do not register. |
Quick start
With the helpers above in your project, checking and registering a domain looks like this:
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());
}