documentation

The .pump resolver API

Turn a .pump domain into the Solana address behind it with a single HTTP request. Every read endpoint is public, CORS-open, cacheable, and requires no authentication.

Overview

How the .pump registry records ownership.

This is an off-chain registry with on-chain payment. Ownership records live in a Postgres database that this service controls. Solana is used for one thing: collecting and verifying a flat 0.1 SOL payment before a domain is recorded.

Practical implications

Resolution is a plain HTTP GET, so it is fast, cacheable at the CDN, and works from any language or runtime without a Solana RPC. The trade-off is that you are trusting this registry's database rather than the chain.

Quickstart

Resolve your first domain in one command.

terminal
curl https://your-domain.com/api/resolve/alice.pump

A registered domain returns 200 with the resolution payload. An unregistered one returns 404. That is the whole integration surface for most applications.

javascript
async function resolveDomain(domain) {
  const res = await fetch(`https://your-domain.com/api/resolve/${domain}`);

  if (res.status === 404) return null;   // not registered
  if (!res.ok) throw new Error(`resolver failed: ${res.status}`);

  const { target } = await res.json();
  return target;                          // base58 address to pay
}

// Accept either form in your send flow.
const recipient = input.endsWith(".pump")
  ? await resolveDomain(input)
  : input;

Conventions

Rules that hold across every endpoint.

Base URL
All paths are relative to your deployment, for example https://your-domain.com.
Format
Requests and responses are JSON. Write endpoints expect a JSON body with Content-Type: application/json.
Authentication
Read endpoints require none. The two write endpoints authenticate with an Ed25519 wallet signature over a single-use nonce, never a session or API key.
CORS
Read endpoints send Access-Control-Allow-Origin: * and answer OPTIONS, so they are callable directly from a browser.
Errors
Non-2xx responses carry a stable machine-readable error.code plus a human-readable error.message. Branch on the code, display the message.
Casing
Domains are normalized to lowercase everywhere. Sending ALICE and alice hits the same record.
error envelope
{
  "error": {
    "code": "INVALID_NAME",
    "message": "Only a-z, 0-9 and hyphens are allowed."
  }
}

Resolve a domain

The endpoint most integrations only ever need.

GET/api/resolve/{domain}

Returns the address a domain points at, plus its owner and resolver records.

Path parameter

domainstring, required
With or without the TLD. alice and alice.pump are equivalent. URL-encode anything you did not construct yourself.

Response fields

namestring
The fully qualified domain, for example alice.pump.
tldstring
Always pump.
ownerbase58
The wallet that controls the record and can repoint or transfer it.
targetbase58
The address to pay. Defaults to the owner and only differs if the owner deliberately repointed the domain.
recordsobject
Optional profile metadata. Keys are limited to url, twitter, github and description. Absent keys are omitted rather than null.
registeredAtISO 8601
When the payment was verified and ownership recorded.

Example

200 OK
{
  "name": "alice.pump",
  "tld": "pump",
  "owner": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
  "target": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
  "records": {
    "url": "https://alice.example",
    "twitter": "@alice",
    "github": "alice",
    "description": "builder"
  },
  "registeredAt": "2026-08-16T11:02:44.180Z"
}
404 Not Found
{
  "error": {
    "code": "NOT_FOUND",
    "message": "alice.pump is not registered."
  }
}

Pay the target, not the owner

owner is who controls the record; target is where value should go. They are identical until the owner repoints the domain, and any integration that pays owner will silently send funds to the wrong place the first time somebody does.

Typed client

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

type Resolution = {
  name: string;
  owner: string;
  target: string;
  records: Partial<Record<"url" | "twitter" | "github" | "description", string>>;
  registeredAt: string | null;
};

export async function resolve(domain: string): Promise<PublicKey | null> {
  const res = await fetch(`https://your-domain.com/api/resolve/${encodeURIComponent(domain)}`, {
    // Resolutions are stable; let the platform cache do the work.
    next: { revalidate: 30 },
  });

  if (!res.ok) return null;

  const data = (await res.json()) as Resolution;
  return new PublicKey(data.target);
}

Check availability

Whether a domain can be registered right now, and why not if it cannot.

GET/api/check?name={name}

Validates the name and reports availability. Always 200; read the available flag.

query parameters
namestring, required
The candidate domain, without the TLD.
response fields
availableboolean
True only if the domain is free and not held by a live reservation.
normalizedstring
The lowercase form that would actually be registered. Empty when the input is unusable.
reasonstring, optional
Present when unavailable: a validation failure, "Already registered.", or "Someone is registering this right now."
priceSol / priceLamportsnumber
The current flat price, so clients never hardcode it.
200 OK
{
  "available": true,
  "normalized": "alice",
  "tld": "pump",
  "priceSol": 0.1,
  "priceLamports": 100000000
}

Availability is advisory

A free result is a snapshot, not a hold. Two users can both see available: true in the same second; the database decides the winner at reservation time and the loser gets a 409. Never treat this endpoint as a lock.

List the registry

Every confirmed domain, newest first.

GET/api/domains

Paginated list of all confirmed registrations, with an optional substring filter.

query parameters
limitnumber, default 50
Page size, capped at 100.
offsetnumber, default 0
Rows to skip. Combine with total for pagination.
qstring, optional
Case-insensitive substring filter on the domain name.
200 OK
{
  "tld": "pump",
  "total": 128,
  "limit": 50,
  "offset": 0,
  "domains": [
    {
      "name": "alice",
      "fqdn": "alice.pump",
      "owner": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
      "target": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
      "registeredAt": "2026-08-16T11:02:44.180Z"
    }
  ]
}

Domains by wallet

Reverse lookup: everything a given address owns.

GET/api/wallet/{address}

Confirmed domains owned by a base58 address, newest first.

path parameter
addressbase58, required
A valid Solana public key. Anything else returns 400 INVALID_WALLET.
200 OK
{
  "owner": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
  "tld": "pump",
  "count": 2,
  "names": [
    {
      "name": "alice",
      "fqdn": "alice.pump",
      "target": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
      "registeredAt": "2026-08-16T11:02:44.180Z",
      "signature": "4Nd1mBQ...",
      "records": {}
    }
  ]
}

Owner, not target

This lists domains by owner_wallet. A domain repointed at another address still belongs to, and is listed under, its owner.

Domain rules

Validation is identical on the availability check and at registration, so a name that passes one always passes the other.

Character seta-z 0-9 -
Lowercase ASCII letters, digits and hyphens only.
Unicoderejected
All non-ASCII input is refused outright rather than normalized. Accented characters, emoji and Cyrillic lookalikes cannot be registered, so there are no homograph domains to defend against.
Length3 to 63
Measured on the normalized name, excluding the TLD.
Hyphensconstrained
No leading or trailing hyphen, and no consecutive hyphens.
Reservedblocked
admin, www, api, support, help, mail, root, system, official, and pump itself.

Why the ASCII check runs first

The character check is applied to the raw input before lowercasing. Several non-ASCII codepoints lowercase into ASCII (U+212A KELVIN SIGN becomes k), so normalizing first would let exactly the lookalikes this rule exists to stop through the gate.

Error codes

Stable identifiers you can branch on. Messages may be reworded; codes will not.

read endpoints
INVALID_NAME400
The domain failed validation. The message says which rule.
INVALID_WALLET400
The supplied address is not a valid base58 public key.
NOT_FOUND404
The domain is not registered.
RATE_LIMITED429
Too many requests from your IP. Honour the Retry-After header.
NOT_CONFIGURED503
The deployment is missing its database credentials.
DB_ERROR500
The registry database could not be reached.
registration endpoints
NAME_UNAVAILABLE409
Taken, or held by a live reservation. Pick another domain.
NO_RESERVATION404
The reservation id does not exist.
RESERVATION_EXPIRED409
The ten-minute window closed before confirmation.
TX_NOT_FOUND425
The transaction is not visible at confirmed commitment yet. Retryable.
TX_FAILED400
The transaction landed but failed on-chain, so no payment was made.
UNDERPAID400
The treasury's balance change was below the required lamports.
MEMO_MISMATCH400
The transaction does not carry this reservation's memo reference.
PAYER_MISMATCH400
A different wallet signed the payment than made the reservation.
SIGNATURE_ALREADY_USED409
That transaction has already been credited to a domain.
BAD_SIGNATURE401
The Ed25519 signature does not verify against the owner wallet.
BAD_NONCE401
The nonce is unknown, already used, or expired. Request a new one.

Rate limits

Applied per IP, per endpoint, on a sliding one-minute window.

/api/check60 / min
Sized for as-you-type search.
/api/domains60 / min
Registry listing.
/api/wallet/{address}60 / min
Reverse lookup.
/api/register/reserve10 / min
Reservations are cheap to make and costly to squat.
/api/register/confirm30 / min
Deliberately generous: someone who has already paid must always be able to retry.
/api/domains/{name}20 / min
Record updates.
/api/domains/{name}/transfer10 / min
Transfers.

Exceeding a limit returns 429 with RATE_LIMITED and a Retry-After header in seconds. /api/resolve is not rate limited, because it is meant to be called at volume and is cached.

Caching

Resolutions are stable, so cache them aggressively.

/api/resolve/{domain}200
public, max-age=15, s-maxage=30, stale-while-revalidate=300
/api/resolve/{domain}404
public, max-age=5. Short, because an unregistered domain can be claimed at any moment.
/api/domains200
public, max-age=10, s-maxage=30, stale-while-revalidate=120
/api/wallet/{address}200
no-store. Owners expect their own list to update immediately.

Because registration never expires, a cached resolution can only go stale when the owner repoints or transfers the domain. If you cache for longer than the headers suggest, invalidate on those two events rather than on a timer.

How registration works

Three steps. Payment is verified server-side against the chain, not reported by the client.

  1. 01

    Reserve

    POST /api/register/reserve with a name and wallet. A pending row is inserted with a fresh memo reference and a ten-minute expiry. If a unique index rejects the insert, you get a 409 and the domain is not yours.

  2. 02

    Pay

    One transaction: a priority fee, a transfer of 100000000 lamports to the treasury, and a Memo instruction carrying the reservation reference. Your wallet signs and sends it.

  3. 03

    Confirm

    POST /api/register/confirm with the signature. The server fetches the transaction itself and verifies it before writing anything.

What confirmation actually checks

Transaction landed
It exists at confirmed commitment and meta.err is null.
Amount
The treasury's balance delta (postBalances - preBalances at its index) is at least 100000000 lamports. The amount is never read from a transfer instruction: a transaction can contain any number of decoy transfers, but the settled balance change cannot be forged.
Memo
A memo instruction's data exactly equals this reservation's reference, tying the payment to this domain and no other.
Payer
The fee payer, which is always account index 0 and always a signer, matches the wallet that made the reservation.
Freshness
The reservation is still pending and inside its ten-minute window.
Single use
The signature has not already been credited. A UNIQUE constraint is the backstop; the server writes and catches rather than checking first.

If confirmation fails after you have paid

The signature is written to local storage the instant your wallet returns it, the reservation row stays pending, and the interface offers a retry that survives a page reload. Retrying with the same signature is idempotent.

Ownership and transfers

Two authenticated endpoints, both gated by a wallet signature rather than a session.

PATCH/api/domains/{name}

Update the resolution target and resolver records.

POST/api/domains/{name}/transfer

Hand the domain to a different wallet.

Both follow the same handshake: request a nonce from GET /api/auth/nonce?wallet=, sign a message containing the domain, the values being written and that nonce, then send the signature. The server rebuilds the identical message from your request body and verifies it against the owner wallet.

Replay protection
The signed bytes cover every value being written, so a captured signature cannot be reused with different values.
Single-use nonces
Nonces expire after five minutes and are burned with a conditional update, so two concurrent requests race on the row and exactly one wins.
Order of operations
The signature is verified before the nonce is consumed, so a bad signature never burns a good nonce.
Transfers repoint
A transfer also resets the target to the new owner. Leaving it on the previous owner's wallet would quietly keep routing value to them.

FAQ

Questions that come up when integrating.

Do I need a Solana RPC?
No. Resolution is plain HTTP. You only need an RPC if you are building your own registration flow.
Can a domain be revoked?
Not through the API. Only a signature from the owner wallet can change ownership or the target.
What happens if I lose my wallet?
The domain stays pointed where it is and cannot be moved. There is no recovery path: ownership changes require a signature from the owner wallet, and nothing overrides that.
Is the price ever different?
Every domain costs the same flat 0.1 SOL regardless of length or demand. Read priceLamports from /api/check rather than hardcoding it.
Which cluster is this?
Mainnet-beta only. There is no devnet deployment and no cluster switch, so every payment is real SOL.
Can I list every domain?
Yes, through /api/domains, or visually on the registry page.