@monadns/sdk
v2.3.3
Published
Monad Name Service SDK - resolve and register .mon names on Monad
Maintainers
Readme
@monadns/sdk
Resolve and register .mon names on the Monad network. The official SDK for the Monad Name Service.
Install
npm install @monadns/sdk viemQuick Start
import { resolveName, lookupAddress } from '@monadns/sdk';
// Forward: name → address
const address = await resolveName('alice.mon');
// '0xa05a8BF1eda5bbC2b3aCAF03D04f77bD7d66Cc47'
// Reverse: address → name
const name = await lookupAddress('0xa05a8BF1eda5bbC2b3aCAF03D04f77bD7d66Cc47');
// 'alice.mon'React Hooks
All hooks are compatible with Next.js App Router, Remix, and any React 17+ framework. The 'use client' directive is included in the build.
npm install @monadns/sdk viem reactDisplay .mon names
import { useMNSDisplay } from '@monadns/sdk/react';
function UserBadge({ address }: { address: string }) {
const { displayName, monName, loading } = useMNSDisplay(address);
return (
<span className={monName ? 'text-purple-400 font-bold' : 'text-gray-400'}>
{loading ? '...' : displayName}
</span>
);
}Resolve input (name or address)
import { useMNSResolve } from '@monadns/sdk/react';
function RecipientInput() {
const { resolve, address, name, loading, error } = useMNSResolve();
return (
<div>
<input
placeholder="Address or .mon name"
onChange={(e) => resolve(e.target.value)}
/>
{loading && <span>Resolving...</span>}
{error && <span className="text-red-500">{error}</span>}
{address && <span className="text-green-500">→ {address}</span>}
</div>
);
}Get avatar
import { useMNSAvatar } from '@monadns/sdk/react';
function Avatar({ name }: { name: string }) {
const { url, loading } = useMNSAvatar(name);
if (loading)
return <div className="animate-pulse w-10 h-10 rounded-full bg-gray-700" />;
return url ? <img src={url} className="w-10 h-10 rounded-full" /> : null;
}Get text records
import { useMNSText } from '@monadns/sdk/react';
function Twitter({ name }: { name: string }) {
const { value: handle } = useMNSText(name, 'com.twitter');
return handle ? <a href={`https://x.com/${handle}`}>@{handle}</a> : null;
}Check ownership and expiry
import { useMNSOwner, useMNSExpiry } from '@monadns/sdk/react';
function NameInfo({ name }: { name: string }) {
const { owner, loading: ownerLoading } = useMNSOwner(name);
const {
daysUntilExpiry,
isExpired,
loading: expiryLoading,
} = useMNSExpiry(name);
if (ownerLoading || expiryLoading) return <div>Loading...</div>;
return (
<div>
<p>Owner: {owner}</p>
{isExpired ? (
<p className="text-red-500">This name has expired!</p>
) : (
<p>Expires in {daysUntilExpiry} days</p>
)}
</div>
);
}Registration (with wagmi)
The SDK returns transaction parameters that work directly with wagmi's useWriteContract:
npm install @monadns/sdk viem wagmi @tanstack/react-queryFull Registration Page
import { useState } from 'react';
import {
useAccount,
useWriteContract,
useWaitForTransactionReceipt,
} from 'wagmi';
import { formatEther } from 'viem';
import { useRegistrationInfo, useMNSRegister } from '@monadns/sdk/react';
import { clearCache } from '@monadns/sdk';
function RegisterPage() {
const [label, setLabel] = useState('');
const { address } = useAccount();
const {
info,
loading: checking,
error: checkError,
} = useRegistrationInfo(label, address);
const { prepare, loading: preparing, error } = useMNSRegister();
const { writeContract, data: hash } = useWriteContract();
const { isLoading: confirming, isSuccess } = useWaitForTransactionReceipt({
hash,
});
const handleRegister = async () => {
const tx = await prepare(label, address!);
if (tx) writeContract(tx);
};
// Clear cache after successful registration so lookups reflect new state
if (isSuccess) clearCache();
return (
<div className="max-w-md mx-auto p-6">
<h1 className="text-2xl font-bold mb-4">Register a .mon name</h1>
<input
className="w-full p-3 border rounded-lg mb-4"
placeholder="Search for a name..."
value={label}
onChange={(e) => setLabel(e.target.value)}
/>
{checking && <p className="text-gray-400">Checking availability...</p>}
{checkError && <p className="text-red-500">❌ {checkError}</p>}
{info && !checking && !checkError && (
<div className="space-y-2">
<p>{info.available ? '✅ Available!' : '❌ Already taken'}</p>
{info.available && (
<>
<p className="text-lg">
{info.isFreebie
? '🎉 FREE for 100 years!'
: `${formatEther(info.price)} MON / year`}
</p>
<button
className="w-full bg-purple-600 text-white p-3 rounded-lg disabled:opacity-50"
onClick={handleRegister}
disabled={preparing || confirming}
>
{confirming
? 'Confirming...'
: preparing
? 'Preparing...'
: `Register ${label}.mon`}
</button>
</>
)}
</div>
)}
{error && <p className="text-red-500 mt-2">❌ {error}</p>}
{isSuccess && (
<p className="text-green-500 mt-4">🎉 {label}.mon is yours!</p>
)}
</div>
);
}Profile Editor (Avatar, Twitter, Bio)
import { useState } from 'react';
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi';
import { useMNSText, useMNSTextRecords } from '@monadns/sdk/react';
function ProfileEditor({ name }: { name: string }) {
const { value: currentAvatar } = useMNSText(name, 'avatar');
const { value: currentTwitter } = useMNSText(name, 'com.twitter');
const { value: currentBio } = useMNSText(name, 'description');
const { setAvatar, setTwitter, setDescription } = useMNSTextRecords();
const { writeContract, data: hash } = useWriteContract();
const { isLoading: saving, isSuccess } = useWaitForTransactionReceipt({
hash,
});
const [avatar, setAvatarUrl] = useState('');
const [twitter, setTwitterHandle] = useState('');
const [bio, setBio] = useState('');
return (
<div className="max-w-md mx-auto p-6 space-y-4">
<h2 className="text-xl font-bold">Edit {name} Profile</h2>
{currentAvatar && (
<img src={currentAvatar} className="w-20 h-20 rounded-full" />
)}
<div>
<label className="block text-sm font-medium mb-1">Avatar URL</label>
<div className="flex gap-2">
<input
className="flex-1 p-2 border rounded"
placeholder={currentAvatar || 'https://example.com/avatar.png'}
value={avatar}
onChange={(e) => setAvatarUrl(e.target.value)}
/>
<button
className="px-4 py-2 bg-purple-600 text-white rounded disabled:opacity-50"
onClick={() => writeContract(setAvatar(name, avatar))}
disabled={saving || !avatar}
>
Save
</button>
</div>
</div>
<div>
<label className="block text-sm font-medium mb-1">Twitter</label>
<div className="flex gap-2">
<input
className="flex-1 p-2 border rounded"
placeholder={currentTwitter || '@handle'}
value={twitter}
onChange={(e) => setTwitterHandle(e.target.value)}
/>
<button
className="px-4 py-2 bg-purple-600 text-white rounded disabled:opacity-50"
onClick={() => writeContract(setTwitter(name, twitter))}
disabled={saving || !twitter}
>
Save
</button>
</div>
</div>
<div>
<label className="block text-sm font-medium mb-1">Bio</label>
<div className="flex gap-2">
<input
className="flex-1 p-2 border rounded"
placeholder={currentBio || 'Tell the world about yourself'}
value={bio}
onChange={(e) => setBio(e.target.value)}
/>
<button
className="px-4 py-2 bg-purple-600 text-white rounded disabled:opacity-50"
onClick={() => writeContract(setDescription(name, bio))}
disabled={saving || !bio}
>
Save
</button>
</div>
</div>
{saving && <p className="text-gray-400">⏳ Saving to blockchain...</p>}
{isSuccess && <p className="text-green-500">✅ Profile updated!</p>}
</div>
);
}Name Validation
The SDK automatically validates names and provides clear error messages:
import { validateMonName } from '@monadns/sdk';
const validation = validateMonName('alice');
if (!validation.valid) {
console.error(validation.error);
}
// Invalid examples that are rejected:
validateMonName('0x123'); // ❌ "Name cannot start with "0x" (reserved for addresses)"
validateMonName('hello--world'); // ❌ "Name cannot contain consecutive hyphens"
validateMonName('-alice'); // ❌ "Name can only contain lowercase letters..."
validateMonName('a'.repeat(64)); // ❌ "Name must be 63 characters or less"
validateMonName(''); // ❌ "Name cannot be empty"
// Valid examples:
validateMonName('alice'); // ✅
validateMonName('a'); // ✅ 1-character names allowed
validateMonName('ab'); // ✅ 2-character names allowed
validateMonName('web3-name'); // ✅ Hyphens allowed (not at start/end)Avatar Best Practices
Recommended Workflow (Enforced by SDK)
import {
validateAvatarFull,
useMNSTextRecords,
MAX_AVATAR_BYTES,
} from '@monadns/sdk/react';
import { useWriteContract } from 'wagmi';
function AvatarUploader({ name }: { name: string }) {
const [error, setError] = useState('');
const { setAvatarValidated } = useMNSTextRecords();
const { writeContract } = useWriteContract();
const handleUpload = async (url: string) => {
try {
setError('');
// SDK validates size automatically (throws if > 50KB)
const tx = await setAvatarValidated(name, url);
writeContract(tx);
} catch (e: any) {
setError(e.message);
// "Avatar file size (125.3KB) exceeds 50KB limit. Please optimize..."
}
};
return (
<div>
<input onChange={(e) => handleUpload(e.target.value)} />
{error && <p className="text-red-500">{error}</p>}
<p className="text-sm text-gray-500">
Max size: {(MAX_AVATAR_BYTES / 1024).toFixed(0)}KB (WebP/SVG
recommended)
</p>
</div>
);
}Size Limits (Automatically Enforced)
- Data URIs: Strict 50KB limit (enforced immediately)
- Remote URLs (IPFS/HTTP): Validated when using
setAvatarValidated() - NFT avatars: No size check (resolved at display time)
Supported Avatar Formats
// ✅ HTTPS URLs
setAvatar('alice.mon', 'https://example.com/avatar.png');
// ✅ IPFS with protocol
setAvatar('alice.mon', 'ipfs://QmX...');
// ✅ Raw IPFS CID (auto-converted)
setAvatar('alice.mon', 'QmX...'); // → ipfs://QmX...
// ✅ Arweave
setAvatar('alice.mon', 'ar://abc123...');
// ✅ NFT avatar
setAvatar('alice.mon', 'eip155:1/erc721:0x.../123');
// ✅ Data URI (with size check)
setAvatar('alice.mon', 'data:image/svg+xml;base64,...');Manual Validation (Advanced)
import { validateAvatarUri, validateAvatarFull } from '@monadns/sdk';
// Quick format check (synchronous)
const validation = validateAvatarUri(avatarUrl);
if (!validation.valid) {
console.error(validation.error);
}
// Full validation including size check (async)
try {
await validateAvatarFull(avatarUrl);
// Safe to use
} catch (error) {
console.error(error.message);
// "Avatar file size (65.3KB) exceeds 50KB limit..."
}
// Check remote file size
import { getRemoteAvatarSize, MAX_AVATAR_BYTES } from '@monadns/sdk';
const size = await getRemoteAvatarSize('https://example.com/avatar.png');
if (size && size > MAX_AVATAR_BYTES) {
alert(`Avatar too large: ${(size / 1024).toFixed(1)}KB. Max is 50KB.`);
}Fallback Avatars
import { getAvatarUrl, DEFAULT_AVATAR_PLACEHOLDER } from '@monadns/sdk';
// Use fallback if avatar not set or fails to resolve
const avatar = await getAvatarUrl('alice.mon', undefined, {
fallback: DEFAULT_AVATAR_PLACEHOLDER,
});Override Validation (Not Recommended)
If you need to bypass validation (not recommended for production):
// Use basic setAvatar() - only validates data URIs
const tx = setAvatar(name, largeAvatarUrl); // Logs warningAdvanced Usage
Check Name Ownership & Expiry
import { getOwner, getExpiry, getResolver } from '@monadns/sdk';
// Check who owns a name
const owner = await getOwner('alice.mon');
console.log('Owner:', owner);
// Check when it expires
const expiry = await getExpiry('alice.mon');
const expiryDate = new Date(expiry * 1000);
const daysLeft = Math.floor(
(expiry * 1000 - Date.now()) / (1000 * 60 * 60 * 24),
);
console.log(`Expires: ${expiryDate.toLocaleDateString()} (${daysLeft} days)`);
// Get resolver contract
const resolver = await getResolver('alice.mon');
console.log('Resolver:', resolver);Check Availability Before Registration
import { getAvailable } from '@monadns/sdk';
const available = await getAvailable('alice.mon');
if (available) {
console.log('alice.mon is available for registration!');
} else {
console.log('alice.mon is already taken');
}API Reference
Core Functions (@monadns/sdk)
| Function | Description |
| ------------------------------------- | --------------------------------------------- |
| resolveName(name) | Forward resolve: alice.mon → 0x... |
| lookupAddress(address) | Reverse resolve: 0x... → alice.mon |
| getDisplayName(address) | Returns .mon name or truncated address |
| resolveInput(input) | Accepts name or address, returns address |
| getTextRecord(name, key) | Get text record (avatar, url, etc.) |
| getAvatarUrl(name, config, options) | Get resolved avatar URL (handles IPFS, NFTs) |
| getAvailable(name) | Check if a name is available for registration |
| getOwner(name) | Get the owner address of a name |
| getResolver(name) | Get the resolver contract address |
| getExpiry(name) | Get expiry timestamp (Unix seconds) |
| clearCache() | Clear the resolution cache |
| getRegistrationInfo(label, address) | Get price, availability, freebie status |
| getRegisterTx(label, address) | Get tx params for registration |
| getSetTextTx(name, key, value) | Get tx params for setting a text record |
| getSetAddrTx(name, address) | Get tx params for updating the address record |
| validateMonName(label) | Validate .mon name format and rules |
| validateAvatarUri(uri) | Validate avatar format and data URI size |
| validateAvatarFull(uri) | Async validation including remote file size |
| getRemoteAvatarSize(url) | Get file size of remote URL (helper for UI) |
React Hooks (@monadns/sdk/react)
| Hook | Returns | Description |
| ------------------------------------- | -------------------------------------------------------------------- | ----------------------------- |
| useMNSName(address) | { name, loading, error } | Reverse resolution |
| useMNSAddress(name) | { address, loading, error } | Forward resolution |
| useMNSDisplay(address) | { displayName, monName, loading } | Display-ready name |
| useMNSResolve() | { resolve, address, name, loading, error } | On-demand resolver for inputs |
| useMNSText(name, key) | { value, loading, error } | Read text records |
| useMNSAvatar(name) | { url, loading, error } | Resolved avatar URL |
| useMNSOwner(name) | { owner, loading, error } | Get name owner |
| useMNSResolver(name) | { resolver, loading, error } | Get resolver address |
| useMNSExpiry(name) | { expiry, expiryDate, daysUntilExpiry, isExpired, loading, error } | Get expiration info |
| useRegistrationInfo(label, address) | { info, loading, error } | Pre-registration data |
| useMNSRegister() | { prepare, tx, loading, error } | Prepare registration tx |
| useMNSTextRecords() | { setAvatar, setAvatarValidated, setTwitter, ... } | Prepare text record txs |
| useMNSAddr() | { setAddr } | Prepare address update tx |
Constants
| Constant | Value |
| ---------------------------- | -------------------------------------------- |
| MAX_AVATAR_BYTES | 51200 (50KB) |
| DEFAULT_AVATAR_PLACEHOLDER | Transparent 1x1 SVG data URI |
| MNS_REGISTRY | 0x13f963486e741c8d3fcdc0a34a910920339a19ce |
| MNS_PUBLIC_RESOLVER | 0xa2eb94c88e55d944aced2066c5cec9b759801f97 |
| MNS_CONTROLLER | 0x98866c55adbc73ec6c272bb3604ddbdee3f282a8 |
| MNS_BASE_REGISTRAR | 0x104a49db9318c284d462841b6940bdb46624ca55 |
Custom RPC
All functions accept an optional config object:
const name = await lookupAddress('0x...', {
rpcUrl: 'https://your-custom-rpc.com',
});Or pass an existing viem client:
import { createPublicClient, http } from 'viem';
import { monad } from 'viem/chains';
const client = createPublicClient({ chain: monad, transport: http() });
const name = await lookupAddress('0x...', { client });Contracts
| Contract | Address |
| ----------------- | -------------------------------------------- |
| Registry | 0x13f963486e741c8d3fcdc0a34a910920339a19ce |
| Public Resolver | 0xa2eb94c88e55d944aced2066c5cec9b759801f97 |
| Controller | 0x98866c55adbc73ec6c272bb3604ddbdee3f282a8 |
| Base Registrar | 0x104a49db9318c284d462841b6940bdb46624ca55 |
| Reverse Registrar | 0x6fff58419ad964f2357590c3f1bae576ebe45f52 |
Chain: Monad Mainnet (Chain ID 143)
Changelog
v2.3.0
New Features:
- Name validation: Added
validateMonName()function with comprehensive validation rules - 1 & 2-character names: Removed artificial 3-character minimum - all lengths now supported
- 0x prefix protection: Names cannot start with "0x" to prevent confusion with addresses
- Automatic validation:
useRegistrationInfo()and write functions now validate names automatically
Improvements:
- Better error messages for invalid names
- Validation happens client-side before RPC calls (faster feedback)
- Consistent validation across all entry points
v2.2.0
New Features:
- Avatar validation:
validateAvatarUri()andvalidateAvatarFull()for size checking - Avatar constants:
MAX_AVATAR_BYTES(50KB limit) andDEFAULT_AVATAR_PLACEHOLDER - Enhanced avatar resolution: Support for raw IPFS CIDs and Arweave URIs
- Validated avatar setter:
setAvatarValidated()hook with automatic size validation - Size helper:
getRemoteAvatarSize()for checking remote file sizes - Fallback support:
getAvatarUrl()now accepts fallback option
Behavior Changes:
- Data URI avatars are now strictly limited to 50KB (throws error if exceeded)
- Setting non-data-URI avatars without validation logs a warning
getAvatarUrl()now handles raw IPFS CIDs (e.g.,QmX...) automatically
v2.1.0
New Features:
getOwner(name)- Get the owner of a namegetResolver(name)- Get the resolver contractgetExpiry(name)- Get expiration timestampuseMNSOwner(name)- React hook for ownership infouseMNSResolver(name)- React hook for resolver infouseMNSExpiry(name)- React hook for expiry info with computed fieldsMNS_BASE_REGISTRARconstant now exported
Breaking Changes:
isRegistered()→getAvailable()(inverted boolean logic)
Compatibility
- Frameworks: React 17+, Next.js (App Router & Pages), Remix, Vite, CRA
- Wallet libraries: wagmi, RainbowKit, ConnectKit, Privy, or any viem-based client
- Non-React: All core functions work without React in any JS/TS environment
- Server-side: Core functions can run in Node.js, edge functions, or API routes
License
Unlicense – public domain, no attribution required.
