DEV Community

Tiago Merlini
Tiago Merlini

Posted on

A boilerplate for hosting dynamic content on ENS names - ENS-Dynamic-kit

The Problem

names traditionally point to static content: a fixed address, an IPFS hash pinned at deploy time, a text record changed manually through a wallet transaction. Every update costs gas. Every change requires a transaction to settle. Content is frozen between updates.

This makes ENS impractical for anything dynamic — a live portfolio, a dapp that changes state, a profile that updates automatically, a subdomain-per-user system.

The Idea
What if an ENS name could point to a live backend?

EIP-3668 (CCIP Read)

makes this possible. Instead of storing data on-chain, the resolver contract tells the client: "go fetch this from a URL, then come back with the signed result." The contract verifies the signature and returns the data — trustlessly.

Combined with

ENSIP-10

wildcard resolution, a single resolver contract can handle any subdomain of your ENS name. One gateway serves thousands of names. Records update in real-time via API — no gas, no transactions, no redeploy.

What this enables

Dynamic ENS profiles — update your address, avatar, social links without paying gas

Subdomain-as-identity — mint subdomains to users, point each at their wallet/profile

Token-gated subdomains — issue holder.yourproject.eth to NFT holders automatically

Live dapp state in ENS — point latest.yourprotocol.eth contenthash at your current frontend

Multi-tenant ENS — one resolver, many tenants, each with their own subdomain namespace

CI/CD for ENS — update app.yourname.eth on every deploy, no wallet needed

Browser-native IPFS — store contenthash on-chain once so Brave/Opera resolve your .eth name directly without CCIP Read

How it works

The contract never stores records. It only stores the gateway URL and the signer address. All data lives in the gateway's SQLite database — fully under your control, instantly updatable.

CCIP Read flow (7 steps):

Client calls resolve(name) on the ENS Registry

Registry forwards to the OffchainResolver contract

Contract reverts with OffchainLookup — includes the gateway URL and calldata

Client calls GET /lookup/:sender/:data on the gateway

Gateway decodes the name, fetches the record from SQLite, signs the ABI-encoded response with its private key

Client calls resolveWithProof(response, extraData) back on the contract

Contract verifies the ECDSA signature matches the registered signer — returns the record

Total round-trips: 2 contract calls + 1 HTTP request. No gas. Instant updates.

IPFS browser resolution (Brave / Opera)

Browsers like Brave resolve .eth names by calling contenthash(bytes32) directly on the resolver — they do not follow CCIP Read. The v2 resolver supports this with an on-chain contenthashes mapping:

The admin UI's ENS → IPFS Browser Resolution → Set On-chain (Brave fix) button does both in one click: updates the gateway DB (for CCIP Read clients) and sends the setContenthash() transaction (for Brave direct resolution). Gas paid once; all clients stay in sync.

Standard ENS resolution (MetaMask, ENS app, viem) works via CCIP Read automatically. For browsers that resolve .eth names natively via the address bar, you need an on-chain contenthash.

Pipeline (all from the admin UI, ENS tab):

Build your frontend as a static export (OUTPUT_STATIC=1 bun run build in the client)

Pin the out/ folder to IPFS — use the Pin to Pinata button (requires a Pinata JWT in settings)

Copy the resulting CID into the CID field

Click Set On-chain (Brave fix) — this does both in one transaction:Updates the gateway DB (so CCIP Read clients get the new CID immediately) Sends setContenthash() on-chain (so Brave/direct eth_call clients resolve correctly)

After the transaction confirms, all clients resolve to the new IPFS content — CCIP Read and Brave alike.

The contenthash is encoded as EIP-1577 CIDv1 (dag-pb, sha2-256) so browsers decode it to a valid bafy... CID and IPFS gateways can serve it.

Text Record Extension Spec (ENS-KIT/1)

Status: Draft — A proposed convention for driving frontend UI from ENS text records. Compatible with any ENS name; no custom resolver required beyond standard text record support.

Text records are the config layer. Every key below maps directly to a UI behaviour on the profile page. Set any record via the admin panel or the push API and it takes effect instantly — no redeploy, no gas.

The full spec is served at .eth/spec (the client includes a /spec route).

Conventions

Keys follow existing ENSIP-5 conventions where they exist (com.twitter, com.github, avatar, url, email)

New keys use lowercase with underscores (pfp_button, pfp_button_2)

Multi-value fields use | as separator (label first, URL second)

All URL fields accept ipfs:// as well as https://

Unknown keys are ignored — forwards compatible

Push update endpoint

Update records from any backend — CI pipeline, webhook, cron job:

Contract
OffchainResolver.sol implements:

resolve(bytes name, bytes data) — ENSIP-10 wildcard entry point, reverts with OffchainLookup

resolveWithProof(bytes response, bytes extraData) — verifies gateway ECDSA signature, returns decoded result

contenthash(bytes32 node) — returns on-chain IPFS contenthash (for Brave / direct browser resolution)

setContenthash(bytes32 node, bytes contenthash) — owner-only, set contenthash on-chain (one gas tx)

supportsInterface — declares IExtendedResolver, IAddrResolver, ITextResolver, IContentHashResolver, IERC165

setSigner(address) — update the signing key without redeploying

setGatewayURLs(string[]) — update the gateway URL without redeploying

Mainnet deployment (v2):

0xa912dF7bb8b0a531800dF47dCD4cfE9bD533d33a

Brave / Opera / Freedom browsers: ens://dinamic.eth

Chrome:

https://dinamic.eth.limo/

Full Post: https://x.com/MerloOfficial/status/2046413347122262125?s=20

This is a early stage live demo

If you wish to contribute visit :

https://github.com/Echo-Merlini/ens-dynamic-kit

Top comments (0)