Register a handle
@x1id/resolve ships instruction builders for every registration path. Each returns an unsigned BuiltInstruction — { programId, keys, data } — that you assemble into a transaction, sign, and relay yourself. The SDK never holds keys, and every discriminator and account order matches the deployed program.
This page targets @x1id/[email protected]. The registry is on X1 mainnet at 8JgnNWi24bq9uzfnT9XmkWxvaWMVgoEs9bu8QsHhLe1P (https://rpc.mainnet.x1.xyz; testnet works the same with https://rpc.testnet.x1.xyz).
Registrations and marketplace actions may be paused. The registry has an emergency lockdown, used for example during a security audit. While it's on, the program rejects new registrations, NFT mints, reserved-name and gift claims, TLD creation, and new listings, offers and auctions (ProtocolLockedDown / MarketPaused). Resolution, transfers, set_primary and records keep working, as do exits such as cancelling a listing, completing an existing sale, and lease renewal. Check the banner on app.x1id.io, or read the flag from the Config account with the SDK's readPauseState.
What ships, and what doesn't
Registration is in the SDK. Only transfer and set_primary are not yet exposed as builders — run those from app.x1id.io for now.
| Flow | Builder(s) |
|---|---|
| Permanent (one-time) | buildRegisterIx (+ fetchConfigTreasury) |
| Lease-to-own | buildRegisterLeasedIx, buildRenewLeaseIx, buildReclaimExpiredLeaseIx |
| Commit–reveal (front-run-safe) | commitmentHash, buildCommitIx, buildRegisterRevealedIx, buildCancelCommitmentIx |
| Claim a gifted name | buildClaimVoucherIx, buildClaimVoucherTokenizedIx |
| Reserved name (admin) | buildRegisterReservedIx |
Price is computed on-chain. register, register_leased and register_revealed all compute their price at execution time (a time ramp over the on-chain tier table that rises over time) — there is no client-supplied price or slippage parameter; the payer pays whatever the program charges as of the landing slot. These builders do not know or assert the price. To show the payer a figure first, simulate the transaction, or read the live /quote/:name REST endpoint.
Deriving PDAs
The builders do not derive PDAs for you (a second on-curve implementation is how wrong addresses get derived silently). Derive them with your runtime's canonical findProgramAddress, or the WasmResolver helpers. The seeds you need:
["config"]— the singleton config account["handle", name_bytes]— the handle being claimed (nameis the canonical form, fromparseName(input).canonical)
Permanent registration
The default: pay the one-time ramp price and claim an unregistered ["handle", name] PDA.
import { buildRegisterIx, fetchConfigTreasury, HandleType } from '@x1id/resolve';
const treasury = await fetchConfigTreasury(rpc, configAccount); // Config.treasury, read fresh
const ix = buildRegisterIx({
programId,
payer, // signer — pays the fee and the Handle account's rent
owner, // need NOT sign — register on someone else's behalf (gifting, onboarding)
config: configAccount, // the ["config"] PDA
treasury, // Config.treasury — admin-rotatable, don't cache across a session
handle, // the ["handle", name] PDA — must be unregistered
name: 'payday-bot', // canonical form
handleType: HandleType.Agent,
// optional revenue-share pair (#7397) — supply BOTH or NEITHER:
// integrator, integratorAllowlist,
});
// assemble, sign with `payer`, relay.HandleType is descriptive only (it gates nothing): Human (0), Merchant (1), Org (2), Agent (3).
The NFT-included default. The product's standard consumer registration folds a tradeable capability pNFT into the single one-time price (see Fees). That path is the program's register_tokenized instruction, which app.x1id.io uses; in @x1id/[email protected] it does not yet have a standalone builder. buildRegisterIx builds the base register (a name-only handle owned by owner), which can be minted into its NFT later. The one tokenizing path with a builder today is the gift claim, buildClaimVoucherTokenizedIx, which registers and mints the pNFT in one instruction.
Lease-to-own
A sibling of register for term leases — the permanent path is untouched and stays the default. Pay a fraction up front and build equity toward the permanent price; payment is in lamports, capped on-chain at the permanent price (offering the full price settles the handle permanently in one instruction).
import { buildRegisterLeasedIx, buildRenewLeaseIx, buildReclaimExpiredLeaseIx, fetchConfigTreasury, HandleType } from '@x1id/resolve';
const treasury = await fetchConfigTreasury(rpc, configAccount);
// 1. Claim under a lease, paying the first installment now:
const leaseIx = buildRegisterLeasedIx({
programId, payer, owner, config: configAccount, treasury, handle,
name: 'studio', handleType: HandleType.Org,
payment: 25_000_000_000n, // lamports toward the lease; >= permanent price settles it outright
});
// 2. Pay more toward a lease — PERMISSIONLESS (anyone may keep someone else's lease alive):
const renewIx = buildRenewLeaseIx({
programId, payer, config: configAccount, treasury, handle, payment: 10_000_000_000n,
});
// 3. Take over a leased handle whose grace period has ENDED, under a fresh lease:
const reclaimIx = buildReclaimExpiredLeaseIx({
programId, payer, owner, config: configAccount, treasury, handle,
handleType: HandleType.Org, payment: 25_000_000_000n,
});The program enforces now >= expires_at + grace before a reclaim succeeds. renew_lease and reclaim_expired_lease are both permissionless by design.
Commit–reveal (front-run-safe)
The anti-front-running path: stake out a hash of your intended name, wait Config.min_commitment_age_secs, then reveal. The name never touches the chain until reveal. The result is a permanent registration, identical to register.
import {
commitmentHash, buildCommitIx, buildRegisterRevealedIx, buildCancelCommitmentIx,
fetchConfigTreasury, HandleType,
} from '@x1id/resolve';
// 1. Generate a secret and persist it BEFORE committing — a lost secret strands the commitment.
const secret = crypto.getRandomValues(new Uint8Array(32));
const hash = await commitmentHash({
name: 'moonshot', owner, handleType: HandleType.Human, secret, programId,
});
// derive ["commitment", hash] with your runtime's findProgramAddress → `commitment`
// 2. Commit — only the hash goes on chain:
const commitIx = buildCommitIx({ programId, payer, commitment, commitmentHash: hash });
// 3. After min_commitment_age_secs (and before max), reveal:
const treasury = await fetchConfigTreasury(rpc, configAccount);
const revealIx = buildRegisterRevealedIx({
programId, payer, owner, config: configAccount, treasury, handle, commitment,
name: 'moonshot', handleType: HandleType.Human, secret,
// optional integrator pair, same contract as register's
});
// Escape hatch — reclaim an unconsumed commitment's rent at any age (payer-only):
const cancelIx = buildCancelCommitmentIx({ programId, payer, commitment });The commitment hash is sha256(name ‖ owner ‖ handle_type ‖ secret ‖ program_id) — the program id in the preimage is domain separation, so a commitment can't replay against a different deployment. The reveal's owner must match the one the commitment hashed, or it fails closed (CommitmentMismatch). The reveal is valid only inside the window: at least min_commitment_age_secs after the commit (CommitmentTooNew) and strictly before max_commitment_age_secs (CommitmentTooOld). Use fetchCommitment(rpc, commitmentAccount) to read a commitment's payer / committedAt.
Persist the secret the moment you generate it, before commit is even sent. Without it the hash can't be recomputed, so the commitment can only ever be cancelled (which at least needs its address, discoverable via getProgramAccounts on the payer).
Claim a gifted name
A gift is a prepaid voucher: the giver escrows the mint price into ["voucher", name], and the recipient claims it later, paying only the transaction fee and the handle's rent. buildClaimVoucherTokenizedIx is the default path the live gift flow uses — it registers the handle and mints its tradeable capability pNFT in one instruction.
import { buildClaimVoucherTokenizedIx } from '@x1id/resolve';
const ix = buildClaimVoucherTokenizedIx({
programId, claimer, config: configAccount, treasury, voucher,
voucherPayer, // the original giver — receives the voucher's rent on close
owner, // who the pNFT + ownership go to (bound recipient, or claimer for an open voucher)
handle, name: 'gift1',
mint, ownerTokenAccount, metadata, masterEdition, tokenRecord,
collectionMint, collectionMetadata, collectionMasterEdition, collectionAuthority,
// integrator — required iff the voucher carries one
});Prepend a compute-budget instruction. The register + 3 Metaplex CPIs exceed the 200k default compute budget — add a ComputeBudget.setComputeUnitLimit (the app uses ~400k) in the same transaction, or the claim fails on-chain.
A name-only variant, buildClaimVoucherIx, claims without minting the pNFT; on a recipient-bound gift only the bound recipient can sign it (VoucherClaimNotRecipient), so a relayer claiming on someone's behalf uses the tokenized path. A gift's expiry can be at most 366 days out (VoucherExpiryTooFar). To create a gift, use buildCreateVoucherIx; to refund an unclaimed one after expiry, buildRefundVoucherIx — see the SDK README.
Reserved names (admin only)
buildRegisterReservedIx builds register_reserved — the admin-only, free registration of a reserved name into the treasury (on success the handle's owner is set on-chain to Config.treasury, not to an arbitrary target). The admin account must equal Config.admin and must sign; there is no price and no treasury-payment account. Reserved and premium names reach the public through the marketplace, not ordinary registration — this builder exists for the operator who seeds that inventory, not for an end user claiming a name.
A treasury-held reserved name can also be released to the verified holder of a matching domain. That path needs the treasury multisig's on-chain approval: open_reserved_claim_approval (permissionless) creates an empty approval account for the name, and set_reserved_claim_approval, signed only by the treasury (NotTreasury otherwise), stamps the sha256 of the one approved domain. The attestor can grant the name only to a holder of that approved domain (ReservedClaimNotApproved otherwise), so neither the admin nor the attestor alone can decide who receives a reserved name.
