npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@monadns/sdk

v2.3.3

Published

Monad Name Service SDK - resolve and register .mon names on Monad

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 viem

Quick 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 react

Display .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-query

Full 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 warning

Advanced 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.mon0x... | | 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() and validateAvatarFull() for size checking
  • Avatar constants: MAX_AVATAR_BYTES (50KB limit) and DEFAULT_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 name
  • getResolver(name) - Get the resolver contract
  • getExpiry(name) - Get expiration timestamp
  • useMNSOwner(name) - React hook for ownership info
  • useMNSResolver(name) - React hook for resolver info
  • useMNSExpiry(name) - React hook for expiry info with computed fields
  • MNS_BASE_REGISTRAR constant 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.