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

@dotk/sdk

v2.1.0

Published

Read-only client for the dotk name registry on Kaspa: resolve, reverse-look-up, price and verify .k names

Readme

@dotk/sdk

This package resolves .k names on Kaspa. It turns a name into an address, and an address into a name. It also says whether a name is free. It only reads, and nothing here needs a wallet.

One call does what most integrators need. recipientFor takes whatever a person typed, a name, a subname or an address, and answers the address to pay, or why there is none. The narrower calls below it answer more about one kind of input, and a field reaches for them when it wants the records behind a name, or to tell a free name from one still registering.

npm install @dotk/sdk

It runs on Node 22.12 or later, and in evergreen browsers. Its typings name fetch and AbortSignal, so a TypeScript consumer compiles with the DOM library or @types/node in its compiler configuration.

examples/browser.html runs the calls below against a live registry. Save the file, open it in a browser, and press a button. It loads the published package from esm.sh, so it needs no build and no wallet.

A recipient field

import { Dotk } from '@dotk/sdk'

declare function pay(address: string, caption?: string): void
declare function show(why?: string): void // a tag from the table below, or the name rule's words

const dotk = new Dotk()
const r = await dotk.recipientFor(input) // never throws on the input

switch (r.kind) {
  case 'address':
    return pay(r.address)
  case 'name':
    return r.address ? pay(r.address) : show(r.fault)
  case 'subname':
    return r.address ? pay(r.address, `set by the owner of ${r.parent.display}`) : show(r.fault)
  case 'neither':
    return show(r.reason)
}

The answer is an arm for what the input is, each with address on it. An address pays itself. A name pays the address the registry names for it, which a node has checked when proven is true, or carries a fault when there is none. A subname pays the address the owner of the parent set under the label, with the parent's own answer under parent, its proof included, so the field captions the payee as that owner's entry. A subname whose parent is unresolved carries fault: 'parent-unresolved', and its parent keeps the name and the display form alone. A non-null address tells those two subname shapes apart, and 'proven' in r.parent tells their two parent shapes apart, because fault is a string on both and narrows nothing. The rest is neither, with the rule's own words in reason.

The tags a Recipient carries that no other answer does:

| Tag | On | When | | ------------------- | --------- | ----------------------------------------------------------------------------------------- | | unresolved | a name | the API serves no active registration: free, registering, owner unknown, or an API behind | | in-covenant | a name | a covenant holds the name, and a covenant has no address to pay | | parent-unresolved | a subname | the parent is unresolved, so there is no card to read |

A subname carries the tags of the table under Subnames besides, except refuted, where recipientFor throws RefutedError instead. fault is optional in the type and always set where address is null, which is why show takes an optional string. A tag you do not know is a refusal. lookup tells the first three causes of unresolved apart, and status().caughtUp shows the fourth.

A wallet that pays no subnames answers the subname case with a refusal, such as case 'subname': return show('not supported'), and never drops it, or that input gets no message at all.

The narrower calls

recipientFor runs classify, then resolveName for a name or resolveSubname for a subname. Each narrower call is an alternative for a field that wants more about one kind of input. resolveName(name) answers a name in full: the address, the proof, the records and the card. resolveSubname(subname) answers a subname in full: the payee, the fault, and the parent's own answer. addressFor(name) and payeeFor(subname) answer the address and nothing else, for code that already knows what it holds. classify(input) sorts the input without asking the network, and lookup(name) tells a free name from one still registering.

The same field by hand, for a wallet that wants lookup's answer on a name that does not pay:

const typed = dotk.classify(input) // never throws

if (typed.kind === 'address') pay(typed.address)
else if (typed.kind === 'name') {
  const to = await dotk.addressFor(typed.name)
  if (to) pay(to)
  else show((await dotk.lookup(typed.name)).kind) // active under a covenant, pending, ownerUnknown, or free
} else if (typed.kind === 'subname') {
  const to = await dotk.payeeFor(typed.display) // `bob.alice.k`, set by the owner of `alice.k`
  if (to) pay(to)
  else show((await dotk.resolveSubname(typed.display))?.fault ?? 'parent-unresolved')
} else show(typed.reason)

addressFor answers null in two cases. Nobody owns the name, or a covenant owns it. A covenant has no address to pay.

lookup says which case you have. It gives one of four answers:

  • An ACTIVE name
  • A name that is still registering
  • A name whose owner the API cannot name right now
  • A key that nobody holds

available, resolveName and recipientFor fold the middle two into their own answer. A field that must not invite a payment into a half-finished registration therefore asks lookup.

A name for an address

await dotk.displayNameFor('kaspatest:qz…') // 'kaspa.k', or null
await dotk.displayNamesFor([a, b]) // ['kaspa.k', null]

An address can own several names. The owner says which one they mean with a primary record, described below. Where no owner says, this call picks one by convention. It takes the shortest name, and it breaks a tie alphabetically.

namesOf lists every name in that order. It puts primary: true on the name that a card claims. When no card claims one, it puts false on every name. Two cards can claim primary at once, and then the node's order settles which one holds it. The newest deed wins, and the lower outpoint breaks a tie. With no node to ask, the claim goes to whichever claimant comes first in the order above.

displayOrder sorts a list that you built yourself.

Use these calls to show a name. To send value, go the other way, through recipientFor or addressFor.

Records

A name can carry records, such as a URL, an avatar, a GitHub handle and a primary flag. They live in a card. A card is a small output that the owner's last transfer minted beside the deed. A name holds one card, so its records reach you as a single map. recipientFor carries no records for a name, and resolveName is the call for them:

const found = await dotk.resolveName('kaspa.k')
if (found) {
  found.records // { url: 'https://kaspa.org', 'com.github': 'kaspanet', primary: true }
  found.card // the card the API lists, with its own `records` and `proven`, or null
}

A key is an ENSIP-5 key where one fits, and free text otherwise. RECORD_KEYS names the ENSIP-5 keys. A value is a string, and primary alone is a boolean. A value that this version does not recognize arrives as { opaque: '<hex>' }, the exact bytes of one CBOR item. Keep that value, and encodeRecords writes it back unchanged.

With a node, the client proves the card against the chain before its records count, and answers proven: true. A card that the chain refutes leaves records empty. Without a node, proven is null and you are taking the API's word. cardsOf(address) lists the cards that a key can reclaim.

Subnames

A name's owner can point a label at an address. bob.alice.k is the label bob on the card of alice.k, which is the subname's parent. The payee is the parent owner's claim, and the chain proves none of it.

recipientFor answers a subname like any other input, with the payee under address and the parent's own answer under parent:

const r = await dotk.recipientFor('bob.alice.k')
if (r.kind === 'subname' && r.address) {
  r.address // the payee, the entry of the owner of alice.k
  r.parent.proven // the node's word on alice.k, which is where a proof mark belongs
}

The narrower calls answer the same subname on its own:

await dotk.payeeFor('bob.alice.k') // the address to pay, or null

const found = await dotk.resolveSubname('bob.alice.k')
found?.payee // the same address, or null
found?.fault // why there is none, such as 'no-such-label'
found?.parent // the parent's own answer, with its address and its proof

dotk.classify('bob.alice.k') // { kind: 'subname', parent: 'alice', label: 'bob', display: 'bob.alice.k' }

Three rules decide a payee. Rule 1 splits the input: the client trims it, lowercases it, strips one .k, and divides the rest at the last dot. Rule 2 finds the parent's card, and a node proves it. Rule 3 reads the label's value, which holds the owner scheme and a 32-byte payload.

Where a rule refuses, fault carries the tag and payee is null. SubnameError.tag carries the same vocabulary for the faults that throw:

| Tag | When | | -------------------- | -------------------------------------------------------------------- | | no-suffix | a dotted input that does not end in .k | | bad-parent | the parent is not a name, or it is k under a label | | bad-label | a segment is not a name or is k, or the label is empty or too long | | no-label | resolveSubname or payeeFor on a bare name | | parent-in-covenant | a covenant owns the parent, and a covenant claims nothing | | no-card | the parent carries no card | | unreadable-card | the card's blob is not a record set | | no-such-label | the card carries no such label | | refuted | the node refutes the parent's card or its deed | | not-bytes | the value is text, a flag, or an item that is not a byte string | | bad-length | a byte string that is not the head 5821 over 33 bytes | | bad-scheme | the scheme byte names no owner scheme | | held-by-covenant | the scheme byte is a covenant id, which nobody can pay | | zero-payload | the payload is 32 zero bytes | | not-a-point | a key scheme whose payload is not a point on the curve |

On a Subname, no-suffix, bad-parent, bad-label and no-label throw, and the rest ride as fault. The derivations below throw every tag they reach, bad-label included, and a subnames row carries that one as its fault. refuted covers the card and the deed alike, and parent.proven tells those two apart. A tag from a newer version arrives as itself, so treat one you do not know as a refusal.

The three tags a Recipient adds are in the table under "A recipient field". They never ride on a Subname and never throw as a SubnameError.

To build an editor or a listing, take the derivations too. subnames(ownerType, records, prefix) is the listing every editor draws from. It answers every sub: entry of a card in key order, as { label, address, fault? }. That address is the payee of one row, and Subname.payee is the payee of one lookup. A row that carries a fault carries address: null with it. subnameOf answers one label. subnameValue writes the value for an owner pair, and subnamePair reads one back. subnameKey builds the record key and holds the label rule.

Show a payee as somebody's entry and never as a fact. alice.k resolves to an address that the chain proves. bob.alice.k resolves to an address that the owner of alice.k asserted. Put the node's mark on the parent and never on the payee. Any name holder can point a label at anybody's key, so never derive a subname from an address.

Calls

A name argument can be what a person typed. The client trims whitespace and folds case, and .k is optional. " Kaspa.K " and kaspa are therefore one name. An address must be on the registry's network. Every call that reaches the network takes a last argument { signal }. recipientFor is the one call for a send field, and the rest each answer one kind of input.

| Call | Answer | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | recipientFor(input) | what a typed string pays: { kind, address, fault?, … } | | classify(input) | { kind: 'name' \| 'subname' \| 'address' \| 'neither', … } | | addressFor(name) | the address to pay, or null | | payeeFor(subname) | the address a subname pays, or null | | resolveName(name) | { address, proven, records, card, … }, or null | | resolveMany(names) | resolveName for every name | | resolveSubname(subname) | { parent, label, display, payee, fault, … }, or null | | namesOf(address) | [{ name, display, proven, primary, records, … }] | | cardsOf(address) | the cards a key can sweep, live or retired | | namesOfMany(addresses) | namesOf for every address | | displayNameFor(address) | the one name to show, or null | | displayNamesFor(addresses) | displayNameFor for every address | | available(name) | whether the name can be registered now | | availableMany(names) | available for every name | | lookup(name) | active | pending | ownerUnknown | free | | history(name, page?) | { entries, total, complete, … }, newest first | | status() | { sameRegistry, healthy, behind, active, … } | | quote(name) | what registering costs, in sompi: fee, the tier paid to the devfund, bond, deposit, gapValue, lockedValue and totalToFund, which is what the wallet must hold | | verify(name, address) | whether the node holds that deed | | verifyPending(name, addr) | whether the node holds that registration's PENDING deed | | normalize(input) | the bare on-chain name. An input that cannot be one throws | | display(name) | how readers show it, 'kaspa.k' | | keyOf(name) | the name key, blake3(name), hex | | ownerOf(address) | { ownerType, owner }, the record a deed stores | | deedAddress(name, address) | the deed address for name under the owner address | | pendingDeedAddress(n, a) | where a registration of n by a sits before it completes | | claimOf(name, address) | the claim that registration publishes, hex |

resolveName answers address: null for a name a covenant owns, and carries ownerCovenantId.

addressFor and resolveName take a name and refuse a subname. payeeFor and resolveSubname take a subname and refuse a bare name. Neither pair ever answers about the other's subject, so no caller can pay a parent's owner for a label. A name call refusing a subname names the two calls that answer one. recipientFor takes either, and its kind says which one it answered about. Its answer is not a Classified: on the subname arms parent is an object, and the parent's bare name is parent.name.

A plural call answers an array as long as the one you passed, slot for slot, so answers[i] belongs to your item i. Repeats cost one request instead of one request each, which is what makes it affordable to name the counterparties of a page of history. Every list in one API answer, the names or the cards of an address and the entries of a history page, is capped at MAX_LIST_ITEMS, and an answer past it is an ApiError. A list of names or cards is derived and judged in chunks of a few hundred that yield between them, so a screen keeps drawing. A cancel or timeoutMs settles the call at once, and the work stops at its next chunk.

history is the one thing that a node cannot answer, so it comes from the API for every configuration of the client. complete says whether the oldest entry held is the registration itself.

Before you trust an API that you did not pick yourself, make sure that status().sameRegistry is true. caughtUp says whether the API is level with the chain right now. When the API is not level, behind says by how much. An API that fell behind answers "no such name" for a name that it has not reached yet.

Never fall back

If a name will not resolve, say so and stop. Do not send to a similar name, to a cached one, or to the raw input. No failure mode here is worth a guess.

Let recipientFor classify, or classify first

recipientFor classifies the input before it resolves anything, and its subname arm is already resolved. With the narrower calls, call classify on the input first. If the user typed an address, none of this is on the send path at all. classify tells you which one you got, and it never throws.

With classify, a subname arrives as its own arm, and that arm names the parent as parent. Code that handles name and address and treats the rest as neither refuses a subname, which is safe. To support one, call payeeFor on the subname arm. recipientFor pays a subname unless the caller tests kind first, so the choice is the caller's line, not the package's.

Branch on ApiError.code, never on detail

Branch on the code. The Errors section below lists every one. Never branch on detail, which is prose for a person and free to be reworded.

Owners change. If a screen needs it, cache a name that you are showing. Ask again every time before you send.

Records are whatever the name's owner put there. avatar and url are arbitrary strings, and javascript: and data:text/html are as valid in them as https:. Before you put one in an <img src> or an <a href>, allow only the schemes that you mean to render.

This package reads and never writes. Nothing here builds, signs or sends a transaction, and it needs no key. quote and available are here so that a flow can price a registration and make sure that the name is free. The flow then hands the registration to whatever will make it, such as @dotk/sdk-tx.

What a node adds

Give the client a node, and the client proves every answer against the chain before you see it. Without a node, an answer comes from the API, and you are taking the API's word.

| Call | Without a node | With a node | | --------------------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | | recipientFor(input) | the API's answer | a name's proven is true where there is a deed, a subname's proof is parent.proven, and a refuted deed or card throws RefutedError | | addressFor(name) | the API's answer | when the chain disagrees, throws RefutedError | | payeeFor(subname) | the API's answer | when the chain disagrees, throws RefutedError | | resolveSubname(subname) | the API's answer | a refuted deed or card is fault: 'refuted', no payee | | resolveName(name), namesOf(address) | proven: null | proven: true or false | | displayNameFor(address) | any primary card | only a card the chain proves can claim | | available(name) | the API's answer | true only once the chain agrees | | verify(name, address) | throws NodeError | true or false |

Nothing else changes. A client with no node is a normal way to use this package. Every call has the same signature with a node and without one, so wiring one in changes no line of a caller's code. Without a node, nothing is proven and nothing is refuted.

Wiring one up

import { Dotk, fromWasm, fromWrpcJson, fromGrpc } from '@dotk/sdk'

const dotk = new Dotk({ node: fromWasm(() => rpcClient) })
fromWasm(() => rpcClient) // the wasm SDK's RpcClient, connected
fromWrpcJson((method, params) => jsonRpc.call(method, params)) // a JSON-RPC transport of your own
fromGrpc((request) => askOverTheStream(request)) // one request on kaspad's stream
  • If your code replaces the client on reconnect, pass a function to fromWasm. The call then uses the current client, and not the one that was there at startup.
  • Take the wasm SDK from a rusty-kaspa release, v2.0.0 or newer. The npm packages named kaspa and kaspa-wasm are 0.13.0 from 2023 and cannot prove anything. This package refuses a client that old with a NodeError, and does not answer false to everything.
  • kaspad's gRPC service is one bidirectional stream, and it has no method to call. fromGrpc therefore takes the function that puts a request on that stream. That function waits for the response that carries getUtxosByAddressesResponse.
  • The node needs --utxoindex.

Or pass anything shaped { getUtxosByAddresses(addresses: string[], options?: { signal? }): Promise<Utxo[]> }, where a Utxo is { address, covenantId?, transactionId?, index?, daaScore? }. transactionId and index are the outpoint, and the client judges a card by it. A node that leaves them out still resolves names. It throws NodeError where a card has to be proven. daaScore orders two cards that claim primary, and a daaScore left out counts as zero. The bundled adapters fill all three fields.

Options

new Dotk({
  api: 'https://api.dotk.name', // the base, without a version; `null` leaves only offline calls
  node: fromWasm(() => rpc), // no default
  timeoutMs: 20_000, // the default; `null` waits for ever
  concurrency: 6, // API requests in flight at once in a batch call
  network: 'testnet-10', // which bundled registry to address; see DEPLOYMENT_NETWORKS
  genesis: manifest, // a deployment this package does not carry, in place of one it does
  fetch: myFetch, // defaults to the global one
})

api is the base that the version segment hangs off. This package appends its own segment, so one setting survives a version bump. Pass https://api.dotk.name, and not https://api.dotk.name/v1. The constructor refuses the second one. It does not double the segment into a path that answers nothing.

api defaults to the directory of whichever registry the client resolved to. There is one directory per chain: https://api.dotk.name on mainnet and https://api-tn10.dotk.name on testnet-10. DIRECTORIES lists them, and directoryFor(network) answers one. A registry that this package carries without a public directory has no default, and the client says so. It does not borrow another chain's host, because every answer from that host carries a covenant id that this client refuses.

network chooses among the registries that this package carries a manifest for, which DEPLOYMENT_NETWORKS lists. Every manifest is built in and none is fetched, so an API can never supply the identity that you judge that same API by. Naming a network that the package does not carry throws, because every address derived under it belongs to a registry that is not there. Leave network out, and you get mainnet where this build carries it, and otherwise the single registry that it does carry.

Errors

Everything this package throws extends DotkError, with one exception: a call you cancel rejects with the reason you aborted with, as you gave it. recipientFor never throws on its input, and the one error a send field catches around it is RefutedError, which needs a node.

| Error | When | | ----------------------- | --------------------------------------------------------------------------------------------------------------------- | | InvalidNameError | the input cannot be a name. message says why | | InvalidAddressError | not an address, or not one on this network | | RefutedError | the node refutes what the API named: a name's deed, a subname's parent deed, or the card the entry sits on | | SubnameError | the input names no subname, the stored entry names no payee, or checkPayload refuses a deed owner. tag says which | | NodeError | the client cannot ask the node, or you gave no node | | ApiError | the API refused or answered something unusable | | RegistryMismatchError | the API serves a different registry | | TimeoutError | the call outlived timeoutMs | | ConfigError | the options do not describe a usable registry, or a call needs the API and api is null |

ApiError.code is the API's own reason. The codes are invalid_name, invalid_address, invalid_key, invalid_owner_type, invalid_query, not_found, not_ready, stale_proof and internal. A code from a newer API arrives as itself. A refusal the API did not label, such as a transport failure, carries no code at all.