nostr-tools is the low-level layer Nostr client authors build on
Tools for developing Nostr clients.
At a glance
- What is it?
- nostr-tools is a TypeScript library of Nostr primitives published on JSR and npm, covering keys, event signing, relays, pools and a long list of NIP modules. It is released into the public domain, the npm version reads 2.25.2, and the project draws its runtime dependencies only from the scure and noble packages.
- Who is it for?
- nostr-tools is the right foundation for a Nostr client written in TypeScript that wants signing, relay handling and NIP support without pulling in a full framework, since it stays deliberately low level and depends only on scure and noble. It is not the right choice if you want a ready made UI or higher level helpers, because the README points at a separate package for that, and on Node.js it needs a WebSocket implementation supplied by you.
- Can I use it commercially?
- Yes. Unlicense is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 4 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A primitives layer, not a client framework
nostr-tools is a set of tools for developing Nostr clients, and the README is explicit about the scope. It says the package provides only lower-level functionality, and that if you want higher-level features you should look at a different package built on top of this one and available only on JSR.
That framing is the first thing to understand about the library. There is no component library, no router, no store and no UI. What there is, in TypeScript, is the set of primitives that any Nostr client needs: secret key generation, public key derivation, event construction, signing, verification, relay connections, a pool that talks to several relays at once, and one module per NIP.
The dependency policy is unusually narrow. The README states that it only depends on the scure and noble packages, which are the audited cryptography and hashing libraries. There is no web framework, no HTTP client and no state manager in the tree.
The repository is a flat list of TypeScript modules at the root, each with its test file beside it. The names are the protocol: nip04, nip05, nip06, nip07, nip10, nip11, nip13, nip17, nip18, nip19, nip21, nip22, nip25, nip27, nip28, nip29, nip30, nip39 and nip40, alongside core.ts, kinds.ts, filter.ts, abstract-pool.ts and abstract-relay.ts. The tests are written with the same bun test runner used for the suite, and there is a benchmarks.ts file for measuring the hot paths.
Subpath exports keep the crypto out of the relay code
The package.json shows a carefully designed export map, and understanding it is the difference between a small bundle and a large one.
The root export points at index.ts, but the interesting entries are the subpaths. There is a core export, a pure export, a wasm export, kinds, filter, abstract-relay, relay and a pool export, each with source, import, require and types conditions. Each subpath resolves to its own file in the lib/esm, lib/cjs and lib/types trees.
The pure entry point is the one to reach for when you only want cryptographic operations with no network code attached. The README's key and event examples import from '@nostr/tools/pure' specifically, which keeps the relay machinery out of a bundle that only signs or verifies.
The module field points at the ESM build and the main field at the CommonJS build, with a types entry for each. The files array lists only lib, so nothing but the built output is published, and sideEffects is false, which tells bundlers they may drop unused imports.
There is also a jsr.json at the root for the JSR distribution, and the package carries a version of 2.25.2 in package.json.
Keys, events and signatures in a few lines
The core API is small enough to read in one sitting. Generating a keypair and deriving the public key looks like this.
import { generateSecretKey, getPublicKey } from '@nostr/tools/pure'
let sk = generateSecretKey() // `sk` is a Uint8Array
let pk = getPublicKey(sk) // `pk` is a hex stringThe secret key is a Uint8Array and the public key is a hex string, which is the shape the rest of the protocol expects. If you want the secret key as hex, the README points at the utils in the noble hashes package, which is already installed as a dependency, so bytesToHex and hexToBytes are one import away.
Building and signing an event is a single call. The event template holds the kind, the timestamp in seconds, the tags and the content, and finalizeEvent adds the pubkey, computes the event id and signs it.
import { finalizeEvent, verifyEvent } from '@nostr/tools/pure'
let event = finalizeEvent({
kind: 1,
created_at: Math.floor(Date.now() / 1000),
tags: [],
content: 'hello',
}, sk)
let isGood = verifyEvent(event)verifyEvent is the counterpart, and having a verification function in the same library is what makes the tool useful for anything that receives events from a relay rather than only for publishing them.
SimplePool is the default, Relay is the escape hatch
The README's advice on relays is blunt: regardless of what you are doing, you should be using a SimplePool. The pool multiplexes across several relays, and the API is built around filters rather than around individual connections.
The examples show the three operations you need. pool.get takes a filter and returns a single event if one is found. pool.querySync takes a filter with a limit and returns matching events. pool.subscribe registers a callback and keeps a subscription open for events that arrive later, with a filter that can be narrowed by kinds and authors.
Publishing goes through pool.publish, and the README notes that it returns a promise per relay, so Promise.any is used to resolve as soon as one relay accepts the event.
The lower level Relay class is available for the cases the pool does not cover. The README calls direct use unrecommended unless you really need fine-grained control over each Relay object, and the example shows why: handling an auth-required close reason and re-subscribing by hand.
On Node.js there is one extra step, because the library expects a browser WebSocket. You install ws and hand it over.
import { useWebSocketImplementation } from '@nostr/tools/pool'
// or import { useWebSocketImplementation } from '@nostr/tools/relay' if you're using the Relay directly
import WebSocket from 'ws'
useWebSocketImplementation(WebSocket)One module per NIP, from NIP-04 to NIP-40
The NIP coverage is where the breadth of the library shows. Each NIP has its own module and its own test file, which keeps the surface predictable.
The README details a few of them. NIP-45 counting is handled by relay.count, which resolves to a number, and relay.countWithHLL, which also returns the HyperLogLog registers when the relay includes them. Across a pool, countMany merges those registers so an event seen on several relays is not counted twice, and it builds the filter for you from a target and a directive such as reactions, reposts, quotes, replies, comments or followers. Relays that do not support COUNT are skipped by countMany, while a single relay.count against one of them rejects, which is a difference worth knowing before you rely on it.
NIP-42 authentication is handled by the pool helpers subscribeMany, subscribeManyEose and subscribeEose, which take an onauth option that signs the challenge and resubscribes for you.
NIP-27 parsing turns event content into blocks, and the example switches on the block type to distinguish text, a reference with an id pointer, one with an identifier pointer, and URL, image, video, audio and relay blocks. NIP-46 is covered by BunkerSigner, which lets an application request signatures and other actions from a remote signer.
The nipXX modules cover a long tail of the protocol, including encrypted direct messages in nip04, nip05 identifiers, nip06 and nip07 key handling, nip10, nip11, nip13, nip17 through nip19, nip21 and nip22, nip25, nip28, nip29, nip30, nip39 and nip40. If you need to check what one of them does, the file name and its test are the documentation.
Building with bun and tsc, publishing to JSR before npm
The justfile is the shortest description of the development loop. The build removes the lib directory, runs bun run build.js and then runs tsc, so the bundler produces the JavaScript and the compiler produces the type declarations.
The test target is a single command.
bun test --timeout 20000The publish target explains the two distribution channels. It removes the optionalDependencies block from package.json with a perl one liner, publishes to JSR with jsr publish --allow-dirty, restores package.json from git, and only then publishes to npm. The comment in the file gives the reason: JSR is stricter and catches errors first, so npm is published second as the fallback.
There are also format and lint targets that run eslint and prettier over the TypeScript files, and a benchmark target that builds benchmarks.ts and runs it under deno, node and bun with a 60 second timeout on each, ignoring failures so one runtime cannot block the others.
The npm version in package.json is 2.25.2, while the only published GitHub release tag is v2.0.0 from 2023-12-19. The library is clearly still being changed, since the last push to the repository was on 2026-10-18, but the tags are not a reliable version marker. Anyone pinning a version should pin the npm or JSR version, not a git tag.
The trade of a low-level library, and where it stops
Choosing nostr-tools means choosing to write the parts it deliberately leaves out. There is no UI, no persistence layer, no account model and no notification handling. The README routes anyone who wants those to a separate higher-level package, which is a fair design, but it means the amount of code you still have to write is substantial.
The dependency narrowness cuts the other way. Depending only on scure and noble means a small audit surface for the cryptography, which is the part that matters most in a signing library. It also means the library cannot hide a bug in a third party HTTP or websocket stack, because it does not have one.
Two concrete limits are visible in the documentation. On Node.js the WebSocket implementation has to be supplied by the application, which is one more moving part in a server deployment. And the counting behaviour is uneven across relays, since countMany skips relays without COUNT support while a direct relay.count against one of those relays rejects, so a client that assumes uniform relay behaviour will need a try and catch around the direct path.
The realistic alternative for a small client is to write the protocol directly against a WebSocket, which is a weekend of work for a single kind and a handful of relays. nostr-tools earns its place when you need signing that is right, NIP coverage that is complete, and pool semantics that handle several relays without you writing that logic.
Editorial conclusion
nostr-tools is the right foundation for a Nostr client written in TypeScript that wants signing, relay handling and NIP support without pulling in a full framework, since it stays deliberately low level and depends only on scure and noble. It is not the right choice if you want a ready made UI or higher level helpers, because the README points at a separate package for that, and on Node.js it needs a WebSocket implementation supplied by you. Before building on it, pick the subpath you need, since core, pure, relay and pool are separate entry points, install ws on the server, and decide whether to consume the JSR build or the npm build, because the npm version and the git tag do not line up.
Frequently asked questions
How do I install nostr-tools?
Add the JSR package with npx jsr add @nostr/tools. If you use TypeScript, the package requires TypeScript 5.0 or newer, and it is also published to npm.
What dependencies does nostr-tools have?
The README states that it only depends on the scure and noble packages. The package.json marks itself side effect free and publishes only the lib directory, so bundlers can drop unused imports.
How do I use nostr-tools on Node.js?
Install ws and pass it to the library. Import useWebSocketImplementation from '@nostr/tools/pool', or from '@nostr/tools/relay' if you use the Relay class directly, then call useWebSocketImplementation(WebSocket).
Should I use SimplePool or Relay?
The README says you should always use SimplePool regardless of what you are doing. Using Relay directly is described as unrecommended unless you really need fine-grained control over each Relay object, for example handling an auth-required close reason by hand.
How do I count events across relays?
Use pool.countMany with a target and a directive such as reactions, reposts, quotes, replies, comments or followers, which builds the filter and merges HyperLogLog registers so the same event is not counted twice. Relays that do not support COUNT are skipped, while a direct relay.count against one of them rejects.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/nbd-wtf-nostr-tools)Was this useful?