Attesting social attributes with ENS
This guide explains how to attach a compact, verifiable attestation to an ENS name. Supported platforms today are:
- X (
com.x) - Telegram (
org.telegram)
The same scheme applies to any platform with a server-verifiable login, and extends to other private attributes (OAuth subject ids, email addresses, and so on).
How it works
An attestation is a signed record written to an ENS text record by the name's owner. Three pieces are involved:
- The attester — a backend service that verifies that you own your wallet (via SIWE) and that you are able to log into your social media account (via OAuth / login widget), then signs the attestation with its key.
- The attestation — a CBOR-encoded envelope with an EIP-191 signature over a reconstructable DAG-CBOR payload (platform, handle or private uid, ENS name, owner address, timestamp). The attester signs; the name owner publishes on-chain.
- The text record — a parameterized key on the ENS name (for example
attestations[com.x][atst.lighthousegov.eth]), holding the attestation envelope.
What gets written on-chain
The user's social media handle is written to their ENS name as a text record. The attestation is written as a separate text record, parameterized by the platform and the attester's ENS name.
alice.eth
├── com.x = "alice"
├── attestations[com.x][attester.eth] = "0xda61747374..."
├── org.telegram = "alice"
└── attestations[org.telegram][attester.eth] = "0xda61747374..."Verifying the attestation
SDK verification
Read the text record, decode the attestation, and verify the attester signature and ownership using the SDK:
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
import { addEnsContracts } from '@ensdomains/ensjs'
import { attestationVerifier } from '@ensmetadata/sdk'
const client = createPublicClient({
chain: addEnsContracts(mainnet),
transport: http(),
}).extend(attestationVerifier({
maxAge: 90 * 24 * 60 * 60, // optional: reject attestations older than 90 days
}))
// Handle attestation — verifies the public handle on the `com.x` text record.
const handleResult = await client.verifyHandleAttestation({
name: 'alice.eth',
platform: 'com.x',
attester: 'atst.lighthousegov.eth', // optional, defaults to this
})
// UID attestation — verifies a private uid you already know (e.g. from OAuth).
const uidResult = await client.verifyUidAttestation({
name: 'alice.eth',
platform: 'com.x',
uid: '12345',
attester: 'atst.lighthousegov.eth', // optional, defaults to this
})Manual verification
To verify an attestation that has been published to an ENS name, first collect the following details:
- The full ENS name (
name.eth) or subname (alice.name.eth) under which the attestation was published. - The address of the manager of the ENS name (or if the address is a wrapped name, the address of the owner).
- The ENS name of the attester, and the attestation record itself.
- Look up which address the attester's ENS name resolves to. This is the signer's address.
Attestation format
Attestations are encoded using CBOR format, identified by tag 1635021684 (0x61747374, the ASCII encoding of "atst"), using the following structure:
Tag(1635021684) [
2, // envelope version
<integer>, // timestamp
<bytes 65>, // signature — EIP-191 over keccak256(payload)
]Payload
The payload for the signature is encoded using DAG-CBOR. The payload can be recreated using the following key names, and the values from the steps listed above:
| Field | Key | Description |
|---|---|---|
| Name | n | The ENS name the attestation is bound to. |
| Address | a | The wallet address that manages the ENS name. |
| Platform | p | Reverse-DNS platform identifier, e.g. com.x or org.telegram |
| Handle | h | The value of the text record matching the platform namespace. (e.g. alice for com.x) |
| Issued at | t | The timestamp from the attestation. |
Encode the above fields as canonical DAG-CBOR, then hash the bytes with keccak256 to get the digest the attester signed. Recover the signer from the digest plus the signature and compare to the address the attester's ENS name currently resolves to.
Private UID attestations
A non-public user identifier (like the stable id exposed during an OAuth flow, or a Telegram numeric id) can also be attested and published to the user's ENS name without revealing the UID itself. Consumers who already know the user's UID can verify the attestation; those who don't cannot recover it from the record.
UID attestations are published to the user's ENS name under the uid record, parameterized by the platform and the attester's ENS name.
alice.eth
├── uid[com.x][attester.eth] = "0xda61747374..."
└── uid[org.telegram][attester.eth] = "0xda61747374..."These attestations follow the same format as the handle variant above; the payload differs in one field:
| Field | Key | Description |
|---|---|---|
| Name | n | The ENS name the attestation is bound to. |
| Address | a | The wallet address that manages the ENS name. |
| Platform | p | Reverse-DNS platform identifier, e.g. com.x or org.telegram |
| UID | u | The non-public user identifier. (e.g. 12345 for com.x) |
| Issued at | t | The timestamp from the attestation. |
Using the above details from the user's ENS name, plus the private UID received from an OAuth flow or another out-of-band method, you can regenerate the signed payload and verify the attestation.