# Address records
URL: /docs/x1id/records

> Read a handle's multi-chain payment records with the staleness rule enforced, and prove control of a non-SVM (ETH / BTC / SVM) address so its record shows as verified.

# Address records

A `@handle` can carry a payment address for several chains — X1, Solana, Ethereum, Bitcoin. Each lives in its own on-chain `Record` account. This page covers **reading** those records (with the staleness rule enforced for you) and **verifying** a non-SVM address so its record shows as proven.

<LlmActions />

<Callout type="info">
  This page targets **`@x1id/resolve@0.21.0`**. The registry is on **X1 mainnet** at `8JgnNWi24bq9uzfnT9XmkWxvaWMVgoEs9bu8QsHhLe1P` (`https://rpc.mainnet.x1.xyz`).
</Callout>

## Reading records

The simplest read is `records()` on a resolver — it takes a handle and applies the staleness rule:

```ts
const all = await x1id.records('@nike'); // readonly HandleRecord[]
```

For a lower-level read (no resolver, just an RPC), use `fetchRecords`. It demands the handle's `registeredAt` and `recordsClearedAt` — the staleness rule needs them, and there is no variant without them — plus an optional `holderEpoch` for tokenized names (see [the staleness rule](#the-staleness-rule)):

```ts
import { fetchRecords, liveRecords, valueToAddress } from '@x1id/resolve';

// handleAccount = the ["handle", name] PDA; registeredAt / recordsClearedAt / holderEpoch from the Handle account
const records = await fetchRecords(rpc, programId, handleAccount, registeredAt, recordsClearedAt, holderEpoch);

for (const r of liveRecords(records)) {
  console.log(r.chain, valueToAddress(r.chain, r.value));
}
```

A `HandleRecord` carries:

| Field       | Meaning                                                                               |
| ----------- | ------------------------------------------------------------------------------------- |
| `account`   | the Record account's address (base58)                                                 |
| `handle`    | the Handle account it hangs off (base58)                                              |
| `coinType`  | SLIP-44 coin type as stored                                                           |
| `chain`     | which `Chain` that maps to (`'X1' \| 'SOL' \| 'ETH' \| 'BTC'`), or `null` if unknown  |
| `value`     | raw bytes as stored on-chain                                                          |
| `address`   | human-readable address — see `valueToAddress`                                         |
| `verified`  | `Record.verified` **gated by staleness**: forced `false` whenever the record is stale |
| `updatedAt` | `Record.updated_at`, unix seconds                                                     |
| `stale`     | true when the record belongs to a previous owner — see below                          |

`fetchRecords` returns **every** record, stale ones flagged, so an owner surface can show what a previous owner left behind. Anything that resolves, displays to a visitor, or counts must take `liveRecords(...)` first. `valueToAddress` renders stored bytes per chain: X1/SOL as base58 pubkeys, ETH as `0x`-hex, BTC as the canonical address string; unknown coin types render as hex so a value is never silently hidden.

## The staleness rule

<Callout type="warn">
  A record that predates the current registration belongs to a **previous, unrelated owner** of the same name. It must never be resolved as this name's address, never counted, and only ever offered to the current owner as something to remove.
</Callout>

A `Handle`'s address is `["handle", name]` — a pure function of the name — so release + re-register lands the new registration at the *same* pubkey, and `Record` PDAs (`["record", handle_pubkey, coin_type]`) hang off that pubkey. A record a previous owner created (possibly `verified: true` for *their* address) is therefore physically attached to the new owner's name with no action by anyone. The program cannot prevent this; the mandatory read-side rule is enforced structurally here. A record is `stale` when **any** of:

* `updated_at < handle.registered_at` — predates the current registration; **or**
* `updated_at < handle.records_cleared_at` — the owner ran `clear_records`, bulk-invalidating records without a transfer; **or**
* `updated_at < handle.holder_epoch` — the name is a capability NFT and changed hands through a **native marketplace sale** (`buy_handle`), which bumps `holder_epoch` instead of `registered_at` (the registration itself does not move on a tokenized sale). `fetchRecords` takes `holderEpoch` for this; pass it for tokenized names.

This applies to **payment resolution**, not just badges — which is why `fetchRecords` requires the epoch anchors and there is no code path that returns an unjudged record.

<Callout type="warn">
  **Plain NFT transfers are not covered.** `holder_epoch` is bumped only by a native marketplace sale. If a capability NFT is moved by a **plain Metaplex transfer** (wallet-to-wallet, or a sale on a generic NFT marketplace), nothing on the `Handle` moves — not `registered_at`, not `holder_epoch` — so the previous holder's records stay **live** and keep resolving as the new holder's payment address. A new holder should review and `clear_records` on any name they acquired by plain transfer; the app surfaces this as a prompt. Closing this on-chain (stamping the holder on every transfer) is tracked as a future change.
</Callout>

## Verifying a non-SVM address record

To make a record show as `verified: true`, the address's owner must prove control. On-chain, the program itself recovers the address from the signature — this SDK never derives or checks an address client-side, it just builds the challenge and packages the wallet's signature.

The flow has three steps: create the (unverified) record, have the wallet sign a challenge, then submit the proof.

### Ethereum (secp256k1 / EIP-191)

```ts
import {
  buildRecordChallenge, buildCreateRecordIx, buildVerifyRecordEthIx, splitEthSignature,
} from '@x1id/resolve';

// 1. create the (unverified) record
const createIx = buildCreateRecordIx({
  programId, payer, owner, handle, record,
  coinType: 60,          // ETH, per SLIP-44
  value: ethAddressBytes, // 20 raw bytes
});

// 2. build the challenge and have the wallet sign it with personal_sign
const challenge = buildRecordChallenge('alice', 60, registeredAt, 'someNonce123');
const sig65 = await wallet.signMessage(challenge); // 65 raw bytes: r || s || v

// 3. verify
const { signature, recoveryId } = splitEthSignature(sig65);
const verifyIx = buildVerifyRecordEthIx({
  programId, owner, handle, record,
  nonce: 'someNonce123', signature, recoveryId,
});
```

`verify_record_eth` hashes the challenge with EIP-191's `"\x19Ethereum Signed Message:\n"` framing — the exact framing every wallet's `personal_sign` produces. There is no EIP-712 variant on-chain. `splitEthSignature` normalizes `v`'s two conventions (27/28 or 0/1).

### Bitcoin (BIP-137)

```ts
import { buildVerifyRecordBtcIx, splitBtcSignature } from '@x1id/resolve';

const sig65 = await wallet.signmessage(challenge); // header(1) || r(32) || s(32)
const { header, signature } = splitBtcSignature(sig65);
const verifyIx = buildVerifyRecordBtcIx({
  programId, owner, handle, record, nonce: 'someNonce123', header, signature,
});
```

`verify_record_btc` derives the address from the recovered pubkey (never trusts a client-supplied one) and accepts only **compressed P2PKH** (header 31–34) and **bech32/P2WPKH** (39–42), mainnet only — uncompressed P2PKH and P2SH-segwit are refused rather than guessed. `splitBtcSignature` passes the header byte through unchanged; most current wallets produce one of the two supported ranges.

### Another SVM address

No challenge needed — the claimed address simply **co-signs** the transaction (X1 and Solana share ed25519, so the signature *is* the proof):

```ts
import { buildVerifyRecordSvmIx } from '@x1id/resolve';

const verifyIx = buildVerifyRecordSvmIx({
  programId, owner, handle, record,
  claimed, // the address being claimed — MUST co-sign this transaction
});
```

### About the challenge

`buildRecordChallenge(handle, coinType, registeredAt, nonce)` builds the exact bytes a wallet must sign — `x1-handles:v1:<handle>:<coinType>:<registeredAt>:<nonce>`, byte-identical to the program's Rust `challenge()`. Unlike the off-chain control-proof flow, the nonce does **not** need to be server-issued — the on-chain transaction landing *is* the single-use event, so any ASCII-alphanumeric nonce (1–64 chars) you pick is fine. Pass `handle` already canonical (run it through `normalizeHandle` first if it might have a leading `@` or mixed case).

## Next

<Cards>
  <Card href="/docs/x1id/sdk" icon="Code" title="SDK reference" description="resolve across chains, reverse, and the records() method." />

  <Card href="/docs/x1id/verification" icon="BadgeCheck" title="Verified identity" description="The blue-tick that sits alongside per-chain record verification." />

  <Card href="/docs/x1id/examples" icon="BookOpen" title="Wallet send flow" description="Put resolution and records into a real recipient field." />
</Cards>
