Offchain Manager SDK
Gasless ENS subname management via @thenamespace/offchain-manager .
Installation
npm install @thenamespace/offchain-manager
Quick Start
- Initialize the client
import { createOffchainClient } from "@thenamespace/offchain-manager";
const client = createOffchainClient({ mode: "mainnet", defaultApiKey: process.env.NAMESPACE_API_KEY, });
Use 'sepolia' for testing, 'mainnet' for production. Get API keys at https://dev.namespace.ninja
- Check availability before creating
createSubname will overwrite an existing subname. Always check first.
const { isAvailable } = await client.isSubnameAvailable("alice.example.eth");
- Create subname with owner
Include owner to enable address→name filtering via getFilteredSubnames .
import { ChainName } from "@thenamespace/offchain-manager";
await client.createSubname({ label: "alice", parentName: "example.eth", owner: "0x1234...", addresses: [{ chain: ChainName.Ethereum, value: "0x1234..." }], texts: [ { key: "name", value: "Alice" }, { key: "url", value: "https://example.com" }, ], contenthash: "ipfs://baf...", metadata: [{ key: "customData", value: "any value" }], });
Metadata is the custom field that the developer want to set to the subname which does not reflect on the ENS name but be used to query through SDK.
- Query and filter subnames
parentName or parentNames is required.
const page = await client.getFilteredSubnames({ parentName: "example.eth", owner: "0x1234...", page: 1, size: 50, });
Workaround: use the owner field when creating subnames, then query via getFilteredSubnames({ parentName, owner }) for address→name lookups. Recommend one address per one subname for clean reverse mapping.
page.items.forEach((s) => console.log(s.fullName, s.texts, s.addresses));
- Read records
const allTexts = await client.getTextRecords("alice.example.eth"); const { record: name } = await client.getTextRecord( "alice.example.eth", "name", );
Core Methods
Subname CRUD
client.createSubname(request: CreateSubnameRequest): Promise<void> client.updateSubname(subname: string, request: UpdateSubnameRequest): Promise<void> client.deleteSubname(fullSubname: string): Promise<void>
Deletion is irreversible — all records are removed.
Queries
client.isSubnameAvailable(fullSubname: string): Promise<{ isAvailable: boolean }> client.getSingleSubname(fullName: string): Promise<SubnameDTO | null> client.getFilteredSubnames(query: QuerySubnamesRequest): Promise<PagedResponse<SubnameDTO[]>>
Address Records
client.addAddressRecord(subname: string, chain: ChainName, value: string): Promise<void> client.deleteAddressRecord(subname: string, chain: ChainName): Promise<void> client.setDefaultEvmAddress(subname: string, value: string): Promise<void>
Text Records
client.addTextRecord(subname: string, key: string, value: string): Promise<void> client.deleteTextRecord(subname: string, key: string): Promise<void> client.getTextRecords(fullSubname: string): Promise<Record<string, string>> client.getTextRecord(fullSubname: string, key: string): Promise<{ record: string }>
Metadata Records
Not resolved onchain. Useful for discovery and filtering via getFilteredSubnames .
client.addDataRecord(fullSubname: string, key: string, data: string): Promise<void> client.deleteDataRecord(subname: string, key: string): Promise<void> client.getDataRecords(fullSubname: string): Promise<Record<string, string>> client.getDataRecord(fullSubname: string, key: string): Promise<{ record: string }>
Do not store sensitive information — metadata is readable by anyone with API access.
API Key Configuration
client.setDefaultApiKey(apiKey: string): void client.setApiKey(ensName: string, apiKey: string): void
Priority: domain-specific key → default address-based key.
Further Reading
-
Types & Enums: See reference.md
-
Error Handling: See errors.md
-
Advanced Patterns: See advanced.md