Machine-to-machine API for upstream applications (e.g. apps that want to register ENS subdomains on behalf of users).
All endpoints are under /api/v1/.
UPSTREAM_ALLOWED_SIGNERS set (comma-separated Ethereum addresses of allowed callers) and VITE_ROOT_DOMAIN configured.
Each request must be signed by a registered upstream application key. The server maintains a whitelist of allowed signer addresses in UPSTREAM_ALLOWED_SIGNERS.
Use eth_sign / personal_sign on the canonical message string:
CometENS:register:{label}:{owner}:{timestamp}
label — subdomain label, lowercase (e.g. alice)owner — checksummed Ethereum address of the new ownertimestamp — Unix seconds (integer); must be within ±60s of server time (anti-replay)import { createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { mainnet } from 'viem/chains'
const account = privateKeyToAccount('0xYOUR_UPSTREAM_PRIVATE_KEY')
const wallet = createWalletClient({ account, chain: mainnet, transport: http() })
const label = 'alice'
const owner = '0xYourUserAddress'
const timestamp = Math.floor(Date.now() / 1000)
const message = `CometENS:register:${label}:${owner}:${timestamp}`
const signature = await wallet.signMessage({ account, message })
import { Wallet } from 'ethers'
const signer = new Wallet('0xYOUR_UPSTREAM_PRIVATE_KEY')
const label = 'alice'
const owner = '0xYourUserAddress'
const timestamp = Math.floor(Date.now() / 1000)
const message = `CometENS:register:${label}:${owner}:${timestamp}`
const signature = await signer.signMessage(message)
Register a subdomain under the configured root domain. Atomically creates the node and sets the ETH address record in one L2 transaction.
| Field | Type | Description |
|---|---|---|
label | string | Subdomain label (1-63 chars, lowercase alphanumeric + hyphen) |
owner | address | Ethereum address of the new subdomain owner |
addr | address (optional) | Address to set as the ETH record (defaults to owner) |
timestamp | number | Unix seconds; must be within ±60s of server time |
signature | hex string | personal_sign over the canonical message |
const timestamp = Math.floor(Date.now() / 1000)
const label = 'alice'
const owner = '0x1234...abcd'
const message = `CometENS:register:${label}:${owner}:${timestamp}`
const signature = await wallet.signMessage({ account, message })
const response = await fetch('https://your-cometens-server/api/v1/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ label, owner, timestamp, signature }),
})
const result = await response.json()
// { ok: true, name: "alice.aastar.eth", node: "0x...", txHash: "0x..." }
{
"ok": true,
"name": "alice.aastar.eth",
"node": "0xabc123...",
"txHash": "0xdef456..."
}
| Status | Error | Meaning |
|---|---|---|
| 400 | Invalid label / Missing signature / etc. | Bad request — check field values |
| 401 | Signer X is not in the allowed list | Your signing key is not whitelisted |
| 401 | Timestamp drift too large | Clocks out of sync; use a fresh timestamp |
| 503 | UPSTREAM_ALLOWED_SIGNERS not configured | Server is not configured for upstream access |
Add to your .env.local:
# Comma-separated Ethereum addresses allowed to call /api/v1/* UPSTREAM_ALLOWED_SIGNERS=0xYourUpstreamSignerAddress1,0xYourUpstreamSignerAddress2
Then restart the dev server (pnpm dev).
Any ENS-compatible library works for reads — no API key needed.
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
const client = createPublicClient({ chain: mainnet, transport: http('YOUR_L1_RPC') })
// Resolve ETH address (via CCIP-Read — automatic)
const address = await client.getEnsAddress({ name: 'alice.aastar.eth' })
// Resolve text record
const twitter = await client.getEnsText({ name: 'alice.aastar.eth', key: 'com.twitter' })
import { JsonRpcProvider } from 'ethers'
const provider = new JsonRpcProvider('YOUR_L1_SEPOLIA_RPC')
const address = await provider.resolveName('alice.aastar.eth')
const twitter = await provider.getResolver('alice.aastar.eth')
.then(r => r?.getText('com.twitter'))
bash scripts/resolve-testnet.sh alice.aastar.eth
UPSTREAM_ALLOWED_SIGNERS and restart the serverPOST /api/v1/register with a fresh timestamp; expect {"ok":true,"txHash":"0x..."}bash scripts/resolve-testnet.sh {label}.aastar.eth — should print the owner addressviem.getEnsAddress() with a Sepolia L1 RPC — should return the registered address