Verified validators
MainnetA handle can prove it belongs to the operator of an X1 validator and earn a gold shield — a verification in a class of its own, above the social blue tick. The blue tick is an off-chain review that X1ID's attestor key signed off on. The validator shield is trustless: no attestor, no human review, no key to trust. Control is proven on-chain, by the only key that can prove it — the vote account's authorized_withdrawer.
These are on-chain instructions on the registry program (8JgnNWi24bq9uzfnT9XmkWxvaWMVgoEs9bu8QsHhLe1P), built into X1ID's own program. The attestation they mint is kind 6 (KIND_VALIDATOR) at the usual ["attestation", handle, kind] PDA.
Why it needs no attestor
Social accounts can't be proven on chain — an X or GitHub handle has no on-chain key, so someone trusted (the X1ID attestor) has to do the off-chain check and sign the result. A validator is different: its vote account is an on-chain account, and it names an authorized_withdrawer — the key with authority over the validator's stake rewards. If you can sign as that key, you demonstrably control the validator. The program checks that signature against the authority it reads straight from the vote account, so the proof is self-contained.
That's why the validator kind sits deliberately one past the attestor-mintable range: create_attestation (the attestor's instruction) refuses any kind >= KIND_COUNT, and KIND_VALIDATOR is KIND_COUNT (6). The trusted attestor key cannot mint a validator attestation even if it wanted to — only the two cryptographic instructions below can.
Both instructions require two authorities to agree:
- The handle's current authority (its owner, or the NFT holder if the handle is tokenized) authorizes linking this validator to the handle.
- The vote account's withdraw authority proves control of the validator.
What stored on the minted attestation:
| Field | Holds |
|---|---|
kind | 6 (KIND_VALIDATOR). |
evidence_hash | the validator's vote account pubkey (32 bytes). |
attestor | the withdraw authority that proved control — a historical fact, not the x1id attestor key. |
attested_at | when the link was (last) stamped; subject to the usual staleness rule. |
Two ways to prove it
Pick the path that fits how the withdraw key is held.
attest_validator — co-sign
The withdraw authority co-signs the same transaction as the handle owner. Simplest when both keys are on hand (e.g. the operator holds both).
attest_validator_signed — detached
The withdraw authority signs a challenge OFFLINE; the handle owner submits alone, carrying that signature in an Ed25519 precompile instruction. For cold/air-gapped withdraw keys, or SSO-based owners.
attest_validator (co-sign)
Two signers on one transaction: the handle's current authority and the vote account's withdraw authority. The program:
- Confirms the supplied
vote_accountis a real vote account — owned by the Vote program, with a recognized version tag — rejecting anything masquerading as one. - Reads
authorized_withdrawerfrom its fixed offset in the vote account data. - Requires the co-signing
withdraw_authorityto equal that key (the trustless proof). - Requires the handle's current authority to sign (owner, or NFT holder if tokenized).
- Mints
Attestation { kind: KIND_VALIDATOR }, storing the vote account inevidence_hashand the withdraw authority inattestor.
attest_validator_signed (detached signature)
When the withdraw key can't co-sign the transaction — it's cold, air-gapped, or on a different machine from the handle owner — the withdraw authority signs a challenge offline instead, and the handle owner submits the transaction alone, carrying that signature in an Ed25519 precompile instruction.
The client places the Ed25519 precompile instruction immediately before attest_validator_signed in the same transaction. The program:
- Validates the vote account and reads the withdraw authority exactly as above.
- Requires the handle's current authority to sign (and be the sole transaction signer).
- Rebuilds the exact challenge (below) from on-chain state.
- Introspects the instruction sysvar: the instruction directly before this one must be the real Ed25519 program, verifying a single signature over that exact challenge, by that exact withdraw authority. Anything else fails with
BadValidatorProof.
Same result as the co-sign path, same trustless guarantee — only the real withdraw key can produce a signature over the challenge.
The challenge
The withdraw authority signs these 96 bytes, in order:
| Segment | Bytes | Value |
|---|---|---|
| Domain prefix | 24 | the ASCII string x1id:validator-attest:v1 |
| Handle PDA | 32 | the ["handle", name] account address |
| Vote account | 32 | the validator's vote account pubkey |
registered_at | 8 | the handle's current registered_at, as an i64 little-endian |
Binding the challenge to the handle's current registered_at is what makes a prior owner's signature unusable after a transfer or re-registration: the epoch in the signed bytes no longer matches, so the proof is rejected — the same discipline the non-SVM record proofs use.
The withdraw authority's signature carries no expiry. It stays valid for that exact (handle, vote account, registered_at) tuple until the handle is re-registered or the withdraw key is rotated. This is intended: a validator attestation is a self-evident cryptographic proof, not an attestor endorsement, so the operator can always re-establish it. Re-submitting only re-stamps the same link and is owner-gated, so it is harmless.
The gold shield
A live validator attestation surfaces as a gold shield next to the handle — distinct from the social blue tick, and a higher class of verification because it is trustless. Where both are present, gold wins: a validator-verified name shows the gold shield as its single badge, with the full detail (the linked vote account, any social platforms) on the handle's own page.
Reading it mirrors the blue tick exactly. A validator attestation is kind 6 in a handle's attestations, with its vote account in evidence_hash. It is subject to the same staleness rule as every other attestation: a stale one (predating the current registered_at, or a clear_records) never lights the shield. See Verified identity for fetchAttestations and how staleness is judged from chain.
import { fetchAttestations, liveAttestations } from '@x1id/resolve';
const atts = await fetchAttestations(rpc, programId, handleAccount, registeredAt, recordsClearedAt);
// The validator attestation is kind 6; its evidence_hash is the vote account.
const validator = liveAttestations(atts).find((a) => a.kind === 6);
if (validator) {
// Show the gold shield. validator.evidenceHash is the vote account pubkey.
}Verify your validator
In the app, linking a validator to your handle follows the detached-signature path — the withdraw key never has to touch the browser.
Enter your vote account
On your handle's settings, choose to verify a validator and paste your validator's vote account address. The app reads its authorized_withdrawer from chain and shows you the exact challenge to sign.
Sign the challenge offline
Sign the shown challenge with your withdraw authority key, wherever it lives — your CLI, a hardware wallet, an air-gapped machine. The key never leaves that environment.
Paste the signature
Paste the resulting signature back into the app. It assembles the transaction: the Ed25519 precompile instruction carrying your signature, immediately followed by attest_validator_signed.
Submit
You (the handle owner) sign and send the transaction. On success, the gold shield appears on your handle — proven on-chain, no attestor involved.
Rotated your withdraw authority? Re-run the flow. The attestation overwrites in place with the new signer, so the stored proof always reflects the current authorized_withdrawer.
