Markdown source for agents: https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/patterns/go-client-baseline.md

Nostr Agent Onboarding · start here · index · source: nostr-dev/docs/patterns/go-client-baseline.md · snapshot 2026-10-10

Paths such as ~/Documents/…, ~/Production Environment/…, repos/… and services on localhost refer to the author's workstation and are not available to you — read them as worked examples of a setup you can recreate.

Pattern: the Go baseline — fiatjaf.com/nostr

Start every Go client, relay or tool on this machine from fiatjaf.com/nostr. It is the protocol author's own library and it covers the whole stack: events, relays, pooling, an outbox-aware client SDK, a relay framework, storage backends, Blossom, GRASP, and roughly forty NIP packages.

Verified 2026-09-21 against the module cache and the live proxy.


1. Why this one

Three separate repos that most Go examples on the internet still import — github.com/nbd-wtf/go-nostr, github.com/fiatjaf/khatru and github.com/fiatjaf/eventstore — were all archived by their owner on 2026-01-24 and are read-only. All three now live as packages inside this one module. go-nostr's README says so:

"This repository is in maintenance mode and adventurous programmers are encouraged to try fiatjaf.com/nostr@master instead."

fiatjaf.com/nostr is the successor to all of them, by the same author, with a reworked API. It is already the de-facto standard here: about two dozen Go modules across ~/Production Environment import it, and relays/khatru-example/ was ported onto it on 2026-09-21.

Archived repo Now
github.com/nbd-wtf/go-nostr fiatjaf.com/nostr
github.com/fiatjaf/khatru fiatjaf.com/nostr/khatru
github.com/fiatjaf/eventstore fiatjaf.com/nostr/eventstore

The khatru port is the one with real API churn: hooks became single functions instead of slices (relay.StoreEvent = f, not append), QueryEvents became QueryStored returning an iter.Seq, ids are nostr.ID, and relay.UseEventstore(store, maxQueryLimit) wires storage, replacement, deletion, counting and the expiration manager in one line. Ready-made acceptance policies live in fiatjaf.com/nostr/khatru/policies; worked examples ship in the module at khatru/examples/{basic-lmdb,blossom,grasp,exclusive}.

2. Getting it

go get fiatjaf.com/nostr

Two things are unusual and worth knowing before you fight them:

It has no tagged releases. Only pseudo-versions. @latest resolves to something like v0.0.0-20260916040958-27e395a0f6e7. So:

The source is not on GitHub — it is Nostr-native. The go-import meta at fiatjaf.com/nostr points at a GRASP host:

https://basspistol.org/npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6/nostrlib.git

Browsable at viewsource.win/npub180cvv…/nostrlib. Which is a nice thing to know and also a dependency you should be aware of: proxy.golang.org caches it, so GOFLAGS=-mod=mod builds keep working if that host blips, but a fresh go get of a brand-new commit does not.

2.1 The Go toolchain trap

The library's go.mod declares go 1.25. /usr/bin/go on this machine is 1.24.4. It works anyway, silently, because GOTOOLCHAIN=auto downloads go1.25.0 on demand — so a module that imports this library builds with 1.25 even though go version prints 1.24.4.

The trap: if you scaffold a new module with go mod init under the default toolchain you get go 1.24, and the import then fails with a version error that reads like a network problem. Put go 1.25 in a new go.mod (every Go project here that uses the library already does), and don't trust go version at the shell — run it inside the module.

3. The package map

Import What
fiatjaf.com/nostr Event, Filter, Relay, Pool, Kind, Tags, keys, envelopes, signing
…/sdk the client SDK: System with caching, data loaders, outbox relay management
…/keyer signing abstractions: plain key, bunker, read-only, keyer.New() from any input
…/khatru the relay framework
…/khatru/blossom Blossom server plugin for a khatru relay
…/khatru/grasp GRASP (NIP-34 git) server plugin
…/eventstore pluggable storage: Bleve, BoltDB, LMDB, in-memory, MMM
…/nipb7/blossom Blossom client — upload/download/list, sets Content-Type from the extension
…/nip5a nsite manifests — parse/build 15128 & 35128, base36 labels, path normalisation
…/nip19 bech32 entities
…/nip44, …/nip17, …/nip59 encryption, private DMs, gift wrap
…/nip46 remote signing (bunker) — see §6 before using
…/nip34 git / GRASP event types
…/nip29 relay-based groups
…/nip65 relay lists / outbox
…/nip77 negentropy sync
plus nip04 05 06 10 11 13 14 22 23 27 31 40 42 45 49 52 53 54 57 60 61 70 73 86 92 94, nipad, schema

Check the tree before you write a helper — a lot of what people hand-roll is already in there. nip5a in particular: every nsite deploy script on this machine predates anyone noticing it exists.

4. The API, and how it differs from go-nostr

The single biggest change: identifiers are fixed-size byte arrays, not hex strings.

type Event struct {
    ID        ID          // [32]byte, not string
    PubKey    PubKey      // [32]byte, not string
    CreatedAt Timestamp
    Kind      Kind        // a named type, not int
    Tags      Tags
    Content   string
    Sig       [64]byte    // not string
}

Most porting pain is this. evt.PubKey == somehex no longer compiles; use nostr.PubKeyFromHex, .Hex(), nostr.MustPubKeyFromHex. It also means the JSON marshalling is strict — the library hard-errors on unknown top-level event fields, which is what forced the OTS sidecar fork (see ~/Production Environment/Nostr-Go-Framework/docs/99-gotchas.md). If you were planning to smuggle an extra field into an event: don't; use a tag or a companion event.

Other shape changes:

sk  := nostr.Generate()                     // SecretKey
pk  := sk.Public()                          // PubKey

evt := nostr.Event{Kind: 1, CreatedAt: nostr.Now(), Content: "hello", Tags: nostr.Tags{}}
evt.Sign(sk)                                // sets ID and Sig
ok := evt.VerifySignature()

r, err := nostr.RelayConnect(ctx, "wss://relay.damus.io", nostr.RelayOptions{})
err = r.Publish(ctx, evt)

// Filter takes one filter, not a Filters slice; Tags is a TagMap
f := nostr.Filter{Kinds: []nostr.Kind{1}, Authors: []nostr.PubKey{pk}, Limit: 10}
sub, err := r.Subscribe(ctx, f, nostr.SubscriptionOptions{})

// Go 1.23 iterators
for evt := range r.QueryEvents(f) { _ = evt }

// Many relays at once
pool := nostr.NewPool()
for evt := range pool.FetchMany(ctx, relayURLs, f, nostr.SubscriptionOptions{}) { _ = evt }
for res := range pool.PublishMany(ctx, relayURLs, evt) { _ = res }

For anything that needs the outbox model, profile/relay-list caching or loaders, use sdk.NewSystem() rather than driving Pool by hand — that is what it is for, and hand-rolled outbox resolution is a recurring source of "why can't it find this user's notes".

5. Picking the right layer

Building Start with
A CLI or one-shot tool root package + Pool
A client that reads other people's data sdk.System (outbox + caching + loaders)
A relay khatru + an eventstore backend
A relay that also serves blobs khatru + khatru/blossom
A git host khatru + khatru/grasp
Uploading blobs nipb7/blossom client
Publishing a static site nip5a + nipb7/blossom — and read nsite-publishing.md
Signing as a user keyer — and read signer-login.md

For Go/wasm clients (Testimony, Cerebrate, the Workshop Board, Idle StarFighter are all this shape) the library works in the browser, with one standing patch: the wasm build needs the library dial patch — see project_testimony_app and Testimony's third_party/nostr/FORK.md.

6. The part you must not use as-is

fiatjaf.com/nostr/nip46 (remote signing) is known-broken for production login. Stock, it waits for EOSE from every relay (one dead relay hangs login forever), drops NIP-04 replies, hard-codes a 30 s sign timeout that ignores BunkerSignTimeout, has a broken pre-connect switch_relays, and waits for the relay OK before reading the reply. wellknownnostrjson.go returns an unassigned named return on NIP-05 bunker lookup, so login silently targets the zero pubkey.

Every Go app on this machine that used it stock has the same bugs. Use the patched copy at ~/Production Environment/Testimony/third_party/nostr/ (see its FORK.md) plus its internal/signer/. Full detail: signer-login.md §3.7 and known-issues.md §6.

This is the one place where "start from the library" does not apply. Everywhere else, start from the library.

7. Conventions for this workspace