Install
openclaw skills install ens-managerRegister ENS names, create subdomains, and publish IPFS sites without manual contract calls
openclaw skills install ens-managerComplete ENS name management: register new .eth names, create subdomains, and publish IPFS content to decentralized gateways.
# Check availability and price (dry run)
node scripts/register-ens-name.js mynewname --dry-run
# Register for 1 year
node scripts/register-ens-name.js mynewname \
--years 1 \
--keystore /path/to/keystore.enc \
--password "your-password"
What happens:
Cost: ~$5-20 USD/year (varies by name length and demand)
node scripts/check-ens-name.js yourname.eth
Shows ownership, wrapped status, resolver, and content hash.
node scripts/create-subdomain-ipfs.js yourname.eth subdomain QmIPFS123... \
--keystore /path/to/keystore.enc \
--password "your-password"
Creates subdomain.yourname.eth and sets its IPFS content hash in one command.
Scenario: You want to own mynewname.eth
node scripts/register-ens-name.js mynewname --dry-run
Output:
🦞 ENS Name Registration
========================
📛 Name: mynewname.eth
⏱️ Duration: 1 year
🔍 Checking availability...
✅ Name is available!
💰 Calculating price...
Registration cost: 0.008 ETH
(for 1 year)
✅ Dry run complete.
node scripts/register-ens-name.js mynewname \
--years 1 \
--keystore ~/.openclaw/workspace/wallet-keystore.enc \
--password "keystore-password"
What happens (three phases):
Phase 1: Commitment (TX 1)
📝 Phase 1: Making commitment...
Commitment: 0xabc123...
Secret: 0xdef456...
TX: 0x789...
Waiting for confirmation...
The commitment hash prevents frontrunning (someone stealing your name by seeing your TX and submitting theirs first with higher gas).
Phase 2: Wait 60 Seconds
⏳ Phase 2: Waiting 60 seconds (anti-frontrunning protection)...
60 seconds remaining...
59 seconds remaining...
...
✅ Wait complete!
This mandatory wait ensures your commitment is on-chain before registration.
Phase 3: Registration (TX 2 with payment)
📝 Phase 3: Registering name...
Sending 0.008 ETH...
TX: 0xghi789...
🎉 Registration complete!
📛 Your ENS name: mynewname.eth
🔍 View on ENS: https://app.ens.domains/mynewname.eth
🔗 Registry TX: https://etherscan.io/tx/0xghi789...
Next steps:
1. Set your address: ens.domains → Records → ETH Address
2. Create subdomains: node create-subdomain-ipfs.js
3. Set reverse record: ens.domains → My Account → Primary Name
Total time: ~2-3 minutes (60s wait + TX confirmations)
Total cost: Registration price + ~$2-4 gas (at 10 gwei)
Scenario: You have a static website and want to publish it at meetup.yourname.eth.limo
yourname.eth (registered via Step 1 or app.ens.domains)1. Add website to IPFS:
ipfs add -r ./website
# Output: added QmABC123... website
2. Create ENS subdomain and set content hash:
node scripts/create-subdomain-ipfs.js yourname.eth meetup QmABC123... \
--keystore ~/.openclaw/workspace/wallet-keystore.enc \
--password "keystore-password"
3. Access your site:
Open https://meetup.yourname.eth.limo in any browser!
Cost: ~$0.05 (at 0.1 gwei base fee)
Scenario: You updated your website and have a new IPFS CID
Option A: Use viem directly
const { createWalletClient, http, namehash } = require('viem');
const { mainnet } = require('viem/chains');
const contentHash = require('content-hash');
const node = namehash('subdomain.yourname.eth');
const encodedHash = '0x' + contentHash.encode('ipfs-ns', newCid);
await walletClient.writeContract({
address: PUBLIC_RESOLVER,
abi: [{
name: 'setContenthash',
type: 'function',
stateMutability: 'nonpayable',
inputs: [
{ name: 'node', type: 'bytes32' },
{ name: 'hash', type: 'bytes' }
],
outputs: []
}],
functionName: 'setContenthash',
args: [node, encodedHash]
});
Option B: Use ENS App
node scripts/check-ens-name.js meetup.yourname.eth
Shows current IPFS CID and eth.limo URL.
All operations use these addresses:
// Core ENS contracts
const ENS_REGISTRY = '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e';
// Registration (.eth names)
const ETH_REGISTRAR_CONTROLLER = '0x253553366Da8546fC250F225fe3d25d0C782303b';
// Wrapped names (modern, with fuses)
const NAME_WRAPPER = '0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401';
// Default resolver (most common)
const PUBLIC_RESOLVER = '0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63';
Where they're used:
.eth names (3-phase process)Problem: If you submit a registration transaction, miners or bots can see it in the mempool and frontrun you (submit their own TX with higher gas to steal the name).
Solution: Commitment scheme prevents this:
What happens:
const commitment = keccak256(
encodePacked(
['string', 'address', 'uint256', 'bytes32', 'address', 'bytes[]', 'bool', 'uint16'],
[
name, // 'mynewname' (without .eth)
owner, // Your wallet address
duration, // 31536000 (1 year in seconds)
secret, // Random bytes32 (generated)
resolver, // PUBLIC_RESOLVER
[], // data (empty for basic registration)
true, // reverseRecord (set primary name)
0 // ownerControlledFuses (0 = none)
]
)
);
// Submit to ETHRegistrarController
await controller.commit(commitment);
Gas cost: 45,000 gas ($1-2 at 10 gwei)
Mandatory 60-second wait ensures:
Technical reason: The contract checks that block.timestamp >= commitmentTimestamp + 60 seconds when you register.
What happens:
await controller.register(
name, // 'mynewname'
owner, // Your address
duration, // 31536000 (1 year)
secret, // Same secret from commitment
resolver, // PUBLIC_RESOLVER
[], // data
true, // reverseRecord
0, // fuses
{ value: price } // Payment in ETH
);
Gas cost: 150,000 gas ($3-5 at 10 gwei) + registration price
Total registration: $4-7 gas + registration price ($5-20/year)
Check if wrapped:
node scripts/check-ens-name.js yourname.eth
Look for "🎁 Wrapped (NameWrapper)" or "📦 Unwrapped (Registry)".
Key differences:
| Feature | Wrapped | Unwrapped |
|---|---|---|
| Subdomain creation | NameWrapper.setSubnodeRecord | Registry.setSubnodeOwner + Registry.setResolver |
| Label parameter | String ("meetup") | Bytes32 hash (keccak256) |
| Owner location | NameWrapper ERC-1155 | Registry mapping |
| Advanced features | Fuses, expiry | None |
The create-subdomain-ipfs.js script handles both automatically.
Gas costs vary based on network congestion. The skill shows real-time costs during dry-run.
Current conditions (0.22 gwei - very low):
Typical conditions (10 gwei - normal):
| Name Length | Cost/Year | Example |
|---|---|---|
| 3 characters | ~$773 | abc.eth |
| 4 characters | ~$193 | cool.eth |
| 5+ characters | ~$6 | hello.eth, example.eth |
5-letter name (most common):
4-letter name:
Recommendation: Use --dry-run to check current costs before registering.
See COSTS.md for detailed cost breakdowns and optimization tips.
The name is already registered. Try:
node scripts/check-ens-name.js <name>.eth
To see who owns it and when it expires.
You need ETH for:
Check balance:
# In script or cast
const balance = await publicClient.getBalance({ address });
console.log('Balance:', formatEther(balance), 'ETH');
If you see this during Phase 3:
Common causes:
gas parameternode scripts/check-ens-name.js yourname.ethhttps://ipfs.io/ipfs/YOUR_CIDconst { keccak256, encodePacked } = require('viem');
// 1. Create subdomain
const parentNode = namehash('yourname.eth');
const labelHash = keccak256(encodePacked(['string'], ['subdomain']));
await walletClient.writeContract({
address: ENS_REGISTRY,
abi: [{
name: 'setSubnodeOwner',
type: 'function',
stateMutability: 'nonpayable',
inputs: [
{ name: 'node', type: 'bytes32' },
{ name: 'label', type: 'bytes32' },
{ name: 'owner', type: 'address' }
],
outputs: [{ name: '', type: 'bytes32' }]
}],
functionName: 'setSubnodeOwner',
args: [parentNode, labelHash, ownerAddress]
});
// 2. Set resolver
const subnode = namehash('subdomain.yourname.eth');
await walletClient.writeContract({
address: ENS_REGISTRY,
abi: [{
name: 'setResolver',
type: 'function',
stateMutability: 'nonpayable',
inputs: [
{ name: 'node', type: 'bytes32' },
{ name: 'resolver', type: 'address' }
],
outputs: []
}],
functionName: 'setResolver',
args: [subnode, PUBLIC_RESOLVER]
});
await walletClient.writeContract({
address: NAME_WRAPPER,
abi: [{
name: 'setSubnodeRecord',
type: 'function',
stateMutability: 'nonpayable',
inputs: [
{ name: 'parentNode', type: 'bytes32' },
{ name: 'label', type: 'string' },
{ name: 'owner', type: 'address' },
{ name: 'resolver', type: 'address' },
{ name: 'ttl', type: 'uint64' },
{ name: 'fuses', type: 'uint32' },
{ name: 'expiry', type: 'uint64' }
],
outputs: [{ name: '', type: 'bytes32' }]
}],
functionName: 'setSubnodeRecord',
args: [
namehash('yourname.eth'), // parent node
'subdomain', // label (string!)
ownerAddress, // owner
PUBLIC_RESOLVER, // resolver
0n, // ttl (0 = default)
0, // fuses (0 = none)
0n // expiry (0 = inherit from parent)
]
});
const contentHash = require('content-hash');
const ipfsCid = 'QmABC123...';
const encoded = '0x' + contentHash.encode('ipfs-ns', ipfsCid);
await walletClient.writeContract({
address: PUBLIC_RESOLVER,
abi: [{
name: 'setContenthash',
type: 'function',
stateMutability: 'nonpayable',
inputs: [
{ name: 'node', type: 'bytes32' },
{ name: 'hash', type: 'bytes' }
],
outputs: []
}],
functionName: 'setContenthash',
args: [namehash('subdomain.yourname.eth'), encodedHash]
});
const contenthash = await publicClient.readContract({
address: resolverAddress,
abi: [{
name: 'contenthash',
type: 'function',
stateMutability: 'view',
inputs: [{ name: 'node', type: 'bytes32' }],
outputs: [{ name: '', type: 'bytes' }]
}],
functionName: 'contenthash',
args: [namehash('subdomain.yourname.eth')]
});
if (contenthash && contenthash !== '0x') {
const decoded = contentHash.decode(contenthash);
console.log('IPFS CID:', decoded);
}
After setting a content hash, the subdomain becomes accessible via:
https://subdomain.yourname.eth.limohttps://subdomain.yourname.eth.linkhttps://ipfs.io/ipfs/QmABC123...https://QmABC123....ipfs.dweb.link/Note: eth.limo/eth.link may take a few minutes to update after setting a new content hash.
references/ens-basics.mdFor detailed ENS concepts, wrapped vs unwrapped differences, and content hash encoding, read references/ens-basics.md.