Integration guide for using the Quartz Nostr KMP library in external projects. Use when: (1) adding Quartz as a Gradle dependency, (2) setting up NostrClient with WebSocket, (3) creating/signing/sending events, (4) building relay subscriptions with Filter, (5) handling keys with KeyPair/NostrSignerInternal, (6) using Bech32 encoding/decoding (NIP-19), (7) platform-specific setup (Android vs JVM/Desktop), (8) NIP-57 zaps, NIP-17 DMs, NIP-44 encryption in external projects, (9) running a relay on Quartz and serving/building its NIP-11 relay information document (application/nostr+json).
Resources
1Install
npx skillscat add vitorpamplona/amethyst/quartz-integration Install via the SkillsCat registry.
Here's a thinking process: 1.
Quartz Integration Guide
Reference for integrating com.vitorpamplona.quartz:quartz into external Nostr KMP projects.
Published artifact: com.vitorpamplona.quartz:quartz:1.14.0 (Maven Central)
Targets: JVM 21+, Android (minSdk 21+), iOS (XCFramework quartz-kmpKit)
License: MIT
1. Gradle Setup
Version Catalog (libs.versions.toml)
[versions]
quartz = "1.14.0"
[libraries]
quartz = { module = "com.vitorpamplona.quartz:quartz", version.ref = "quartz" }build.gradle.kts (KMP project)
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.quartz)
}
}
}Android-only project
dependencies {
implementation("com.vitorpamplona.quartz:quartz:1.14.0")
}Transitive dependencies pulled in automatically
Quartz exposes these as api (you get them transitively):
| Dependency | Used for |
|---|---|
fr.acinq.secp256k1:secp256k1-kmp-* |
Schnorr signing |
com.github.anthonynsimon:rfc3986-normalizer |
Relay URL normalization |
com.fasterxml.jackson.module:jackson-module-kotlin |
Event JSON parsing |
For Android, add to build.gradle.kts:
android {
packaging {
resources.excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}2. Key Concepts
Core Types
typealias HexKey = String // 64-char hex string (pubkey, event id, sig)
typealias Kind = Int // Event kind number
typealias TagArray = Array<Array<String>>Event Anatomy
@Immutable
open class Event(
val id: HexKey, // SHA-256 of canonical JSON (64 hex chars)
val pubKey: HexKey, // Author public key (64 hex chars)
val createdAt: Long, // Unix timestamp (seconds)
val kind: Kind, // Event type
val tags: TagArray, // [["e","eventid"], ["p","pubkey"], ...]
val content: String,
val sig: HexKey, // Schnorr signature (128 hex chars)
)Kind Classification
// Regular events — stored by relays forever
val isRegular = kind in 1..9999
// Replaceable events — relay keeps only latest per (pubkey, kind)
val isReplaceable = kind == 0 || kind == 3 || kind in 10000..19999
// Addressable events — relay keeps latest per (pubkey, kind, d-tag)
val isAddressable = kind in 30000..39999
// Ephemeral events — relays don't persist
val isEphemeral = kind in 20000..299993. Key Management
Generate a new keypair
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
// Generate fresh random keys
val keyPair = KeyPair()
// From existing private key bytes
val keyPair = KeyPair(privKey = myPrivKeyBytes)
// Read-only (public key only, cannot sign)
val keyPair = KeyPair(pubKey = myPubKeyBytes)
// Access
val pubKeyHex: String = keyPair.pubKey.toHexKey()
val privKeyHex: String? = keyPair.privKey?.toHexKey()Convert between formats
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
// ByteArray → hex
val hex = byteArray.toHexKey()
// hex → ByteArray
val bytes = hex.hexToByteArray()
// Bech32 import (npub, nsec)
val parsed = Nip19Parser.uriToRoute("npub1abc...")
// or
val parsed = Nip19Parser.uriToRoute("nsec1abc...")Hex ↔ ByteArray is a first-class utility in Quartz — see §3.1 Hex utilities below.
3.1 Hex utilities (HexKey ↔ ByteArray)
Nostr keys, event ids and signatures travel as lower-case hex strings. Quartz
models this with the HexKey typealias (just a String) plus extension
functions — do not write your own byte loop or pull in a third-party codec.
Packages: com.vitorpamplona.quartz.nip01Core.core (the extensions) andcom.vitorpamplona.quartz.utils (the underlying Hex object).
import com.vitorpamplona.quartz.nip01Core.core.HexKey // typealias = String
import com.vitorpamplona.quartz.nip01Core.core.toHexKey // ByteArray → hex
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray // hex → ByteArray
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull
import com.vitorpamplona.quartz.nip01Core.core.isValid
import com.vitorpamplona.quartz.utils.Hex
// Encode / decode
val hex: HexKey = pubKeyBytes.toHexKey() // lower-case, 2 chars per byte
val bytes: ByteArray = hex.hexToByteArray() // throws on odd length
// Untrusted input → decode safely
val maybe: ByteArray? = userInput.hexToByteArrayOrNull() // null if not valid hex
// Validate without decoding (no allocation)
Hex.isHex(userInput) // even-length, all hex digits (any length)
Hex.isHex64(userInput) // fast path for a 32-byte key/id (checks first 64 chars)
hex.isValid() // 64 chars AND valid hex (pubkey / event-id shape)
// Compare a hex string to raw bytes without decoding
Hex.isEqual(incomingHexId, myIdBytes)| Need | Call | Notes |
|---|---|---|
| ByteArray → hex | bytes.toHexKey() |
lower-case output |
| hex → ByteArray (strict) | hex.hexToByteArray() |
throws on odd length |
| hex → ByteArray (safe) | hex.hexToByteArrayOrNull() |
null on invalid hex |
| is this valid hex? | Hex.isHex(s) / Hex.isHex64(s) |
isHex64 ~30% faster for keys/ids |
| is this a pubkey/id shape? | hex.isValid() |
64 chars + valid hex |
| hex == bytes? | Hex.isEqual(hex, bytes) |
no decode allocation |
Constants PUBKEY_LENGTH and EVENT_ID_LENGTH (both 64) live in the samenip01Core.core package.
3.2 Everyday utilities (time, random, hashing, bech32, base64)
These small helpers exist so you don't reinvent them — and several have a
footgun the built-in avoids. Prefer them over stdlib/hand-rolled equivalents.
Time — TimeUtils (com.vitorpamplona.quartz.utils). Everything is in Unix
seconds (what created_at and filter since/until use), not millis.
import com.vitorpamplona.quartz.utils.TimeUtils
val createdAt = TimeUtils.now() // seconds — for created_at. NOT currentTimeMillis()/1000
val since = TimeUtils.oneDayAgo() // relative filter bounds: oneHourAgo(), fiveMinutesAgo()…
val fresh = TimeUtils.withinTenMinutes(event.createdAt) // NIP-42/NIP-98 freshness
// TimeUtils.nowMillis() is the only millisecond helper — non-protocol use only.Secure random — RandomInstance (utils). Backed by SecureRandom; use it
for anything security-sensitive instead of kotlin.random.Random.
import com.vitorpamplona.quartz.utils.RandomInstance
val nonce = RandomInstance.bytes(32) // nonces, salts, keys
val subId = RandomInstance.randomChars() // 16-char [a-zA-Z0-9] subscription idHashing — sha256(...) + EventHasher. sha256 is the raw primitive; to
compute/verify an event id use EventHasher, which canonically serializes[0, pubkey, created_at, kind, tags, content] before hashing (getting this wrong
is what makes relays reject an event). Typed builders already do this for you.
import com.vitorpamplona.quartz.utils.sha256.sha256
import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher
val digest = sha256(bytes) // raw 32-byte hash
val id = EventHasher.hashId(pubKey, createdAt, kind, tags, content)
val valid = EventHasher.hashIdCheck(event.id, event.pubKey, event.createdAt, event.kind, event.tags, event.content)Bech32. For npub/nsec/note/… prefer the NIP-19 layer (ByteArray.toNpub(),Nip19Parser.uriToRoute(...) — see §10). Drop to the low-levelBech32 object (nip19Bech32.bech32) only for a custom prefix:
import com.vitorpamplona.quartz.nip19Bech32.bech32.Bech32
import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes
val addr = Bech32.encodeBytes("npub", pubKeyBytes, Bech32.Encoding.Bech32)
val bytes = "npub1...".bechToBytes("npub") // decode + assert the prefixBase64. Quartz has no wrapper — use the Kotlin stdlib kotlin.io.encoding.Base64
directly, and match the variant the spec wants: NIP-44/NIP-04 payloads useBase64.Default (standard, padded); url-safe contexts use Base64.UrlSafe
(configure padding via .withPadding(...)).
| Need | Call |
|---|---|
Now (event created_at) |
TimeUtils.now() (seconds) |
| Relative filter bound | TimeUtils.oneDayAgo() / oneHourAgo() / … |
| Secure random bytes | RandomInstance.bytes(n) |
| Subscription id | RandomInstance.randomChars() |
| Raw hash | sha256(bytes) |
| Event id / verify | EventHasher.hashId(...) / hashIdCheck(...) |
| Bech32 custom prefix | Bech32.encodeBytes(hrp, bytes, enc) / s.bechToBytes(hrp) |
| Base64 | kotlin.io.encoding.Base64 (.Default / .UrlSafe) |
4. Signing Events
NostrSignerInternal (local key, JVM + Android)
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
val keyPair = KeyPair()
val signer = NostrSignerInternal(keyPair)
// Sign any EventTemplate
val template = TextNoteEvent.build("Hello Nostr!")
val signedEvent: TextNoteEvent = signer.sign(template)NostrSignerSync (synchronous, for testing)
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
val signerSync = NostrSignerSync(keyPair)
val event = signerSync.sign<TextNoteEvent>(
createdAt = TimeUtils.now(),
kind = 1,
tags = emptyArray(),
content = "Hello!"
)NostrSigner interface (for custom signers)
abstract class NostrSigner(val pubKey: HexKey) {
abstract fun isWriteable(): Boolean
abstract suspend fun <T : Event> sign(createdAt: Long, kind: Int, tags: Array<Array<String>>, content: String): T
abstract suspend fun nip04Encrypt(plaintext: String, toPublicKey: HexKey): String
abstract suspend fun nip04Decrypt(ciphertext: String, fromPublicKey: HexKey): String
abstract suspend fun nip44Encrypt(plaintext: String, toPublicKey: HexKey): String
abstract suspend fun nip44Decrypt(ciphertext: String, fromPublicKey: HexKey): String
abstract suspend fun deriveKey(nonce: HexKey): HexKey
abstract fun hasForegroundSupport(): Boolean
// Convenience: auto-detects NIP-04 vs NIP-44 by ciphertext format
suspend fun decrypt(encryptedContent: String, fromPublicKey: HexKey): String
}5. Creating Events
Using typed event builders (recommended)
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
// Kind 1 — Text note
val template = TextNoteEvent.build("Hello Nostr!")
val event: TextNoteEvent = signer.sign(template)
// Kind 1 — Reply
val replyTemplate = TextNoteEvent.build(
note = "Interesting thread!",
replyingTo = originalEventHintBundle
)
// Kind 7 — Reaction
val reactionTemplate = ReactionEvent.build(
content = "+", // "+" = like, "-" = dislike, emoji = custom
originalNote = targetEvent
)Using low-level Event.build DSL
import com.vitorpamplona.quartz.nip01Core.core.Event
val template = Event.build(
kind = 1,
content = "Hello world",
createdAt = TimeUtils.now()
) {
// TagArrayBuilder DSL
add(arrayOf("p", mentionedPubKey))
add(arrayOf("t", "nostr"))
add(arrayOf("subject", "Greeting"))
}
val event: Event = signer.sign(template)TagArrayBuilder DSL methods
// In the DSL lambda:
add(arrayOf("tagname", "value")) // append
addFirst(arrayOf("tagname", "value")) // prepend
addUnique(arrayOf("d", "my-slug")) // replace all tags with same name
addAll(listOf(arrayOf("t", "tag1"), ...)) // bulk add
remove("tagname") // remove all with this name6. Relay Client Setup (JVM / Android)
The relay client requires an OkHttp WebSocket builder (available on JVM + Android).
Minimal setup
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
import com.vitorpamplona.quartz.nip01Core.relay.sockets.okhttp.BasicOkHttpWebSocket
import okhttp3.OkHttpClient
// Build the WebSocket factory
val okHttpClient = OkHttpClient.Builder().build()
val wsBuilder = BasicOkHttpWebSocket.Builder { _ -> okHttpClient }
// Create client (manages its own CoroutineScope internally)
val nostrClient = NostrClient(websocketBuilder = wsBuilder)
nostrClient.connect()With custom OkHttpClient per relay
val wsBuilder = BasicOkHttpWebSocket.Builder { normalizedUrl ->
if (normalizedUrl.url.contains(".onion")) {
torEnabledOkHttpClient // Tor proxy for .onion relays
} else {
regularOkHttpClient
}
}With custom CoroutineScope
val appScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
val nostrClient = NostrClient(wsBuilder, scope = appScope)7. Subscribing to Events
Normalize relay URLs first
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
// Returns NormalizedRelayUrl (wrapper with validated wss:// URL)
val relayUrl = RelayUrlNormalizer.normalize("wss://relay.damus.io")
val relayUrlOrNull = RelayUrlNormalizer.normalizeOrNull("wss://relay.damus.io")
// Handles common fixes: https:// → wss://, strips whitespace, etc.Build a Filter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
// Fetch a user's notes
val filter = Filter(
authors = listOf(pubKeyHex),
kinds = listOf(1),
limit = 50
)
// Since a timestamp
val filter = Filter(
kinds = listOf(1, 6),
since = System.currentTimeMillis() / 1000 - 3600 // last hour
)
// By event tags
val filter = Filter(
kinds = listOf(7),
tags = mapOf("e" to listOf(eventId)) // reactions to an event
)
// AND tag filter (NIP-91)
val filter = Filter(
kinds = listOf(1),
tagsAll = mapOf(
"t" to listOf("nostr"),
"p" to listOf(specificPubKey)
)
)
// Full-text search (NIP-50)
val filter = Filter(
kinds = listOf(1),
search = "bitcoin lightning"
)Open a subscription
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EventMessage
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EoseMessage
val relay = RelayUrlNormalizer.normalize("wss://relay.damus.io")
val subId = "my-sub-${System.currentTimeMillis()}"
val filtersMap = mapOf(relay to listOf(filter))
nostrClient.openReqSubscription(
subId = subId,
filters = filtersMap,
listener = object : IRequestListener {
override fun onEvent(subId: String, event: Event, relay: IRelayClient) {
println("Got event: ${event.id}")
}
override fun onEOSE(subId: String, relay: IRelayClient) {
println("End of stored events from ${relay.url}")
}
}
)
// Close when done
nostrClient.close(subId)Global relay listener
nostrClient.subscribe(object : IRelayClientListener {
override fun onIncomingMessage(relay: IRelayClient, msgStr: String, msg: Message) {
when (msg) {
is EventMessage -> handleEvent(msg.subscriptionId, msg.event)
is EoseMessage -> handleEose(msg.subscriptionId)
else -> {}
}
}
override fun onConnected(relay: IRelayClient, pingMillis: Int, compressed: Boolean) {
println("Connected to ${relay.url} in ${pingMillis}ms")
}
override fun onDisconnected(relay: IRelayClient) {
println("Disconnected from ${relay.url}")
}
// other callbacks: onConnecting, onSent, onCannotConnect
})8. Publishing Events
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
val relaySet = setOf(
RelayUrlNormalizer.normalize("wss://relay.damus.io"),
RelayUrlNormalizer.normalize("wss://nos.lol"),
)
// Sign the event
val template = TextNoteEvent.build("Hello Nostr!")
val event: TextNoteEvent = signer.sign(template)
// Send to relays (handles retry + reconnect automatically)
nostrClient.send(event, relaySet)9. Event Serialization
import com.vitorpamplona.quartz.nip01Core.core.Event
// Serialize to JSON string
val json: String = event.toJson()
// Parse from JSON string
val event: Event = Event.fromJson(json)
// Null-safe parse
val event: Event? = Event.fromJsonOrNull(json)
// Specific typed parse (returns base Event, cast if needed)
val textNote = Event.fromJson(json) as? TextNoteEvent10. Bech32 Encoding / Decoding (NIP-19)
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
import com.vitorpamplona.quartz.nip19Bech32.entities.NNote
import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
// Decode any bech32 entity (plain or nostr:-prefixed).
// uriToRoute() returns Nip19Parser.ParseReturn? — the parsed Entity is in .entity
when (val entity = Nip19Parser.uriToRoute(input)?.entity) {
is NPub -> println("pubkey: ${entity.hex}")
is NNote -> println("event id: ${entity.hex}")
is NEvent -> println("event: ${entity.hex}, relays: ${entity.relay}")
is NProfile -> println("profile: ${entity.hex}")
is NAddress -> println("address: ${entity.aTag()}")
null -> println("not a valid bech32 entity")
else -> {}
}
// Encode: ByteArray extensions from nip19Bech32/ByteArrayExt.kt
val npub = pubkeyBytes.toNpub() // also toNsec(), toNote(), ...
// TLV entities with relay hints (relays: List<NormalizedRelayUrl>)
val nevent = NEvent.create(eventIdHex, authorHex, kind, relays)11. Encryption
NIP-44 (modern, recommended)
// Via signer (preferred)
val encrypted = signer.nip44Encrypt(
plaintext = "Secret message",
toPublicKey = recipientPubKeyHex
)
val decrypted = signer.nip44Decrypt(
ciphertext = encrypted,
fromPublicKey = senderPubKeyHex
)
// Auto-detect format (NIP-04 or NIP-44)
val plaintext = signer.decrypt(encryptedContent, fromPublicKeyHex)NIP-04 (legacy, avoid for new code)
val encrypted = signer.nip04Encrypt(plaintext, recipientPubKeyHex)
val decrypted = signer.nip04Decrypt(ciphertext, senderPubKeyHex)12. Common NIP Event Builders
NIP-02 — Follow list (kind 3)
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
val template = ContactListEvent.build(
follows = listOf(
ContactListEvent.Contact(pubKey = alicePubKey, relayUrl = "wss://relay.damus.io", petname = "alice"),
ContactListEvent.Contact(pubKey = bobPubKey)
)
)NIP-25 — Reaction (kind 7)
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
val like = ReactionEvent.build("+", targetEvent)
val dislike = ReactionEvent.build("-", targetEvent)
val custom = ReactionEvent.build("🤙", targetEvent)NIP-57 — Zap request (kind 9734)
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
val template = LnZapRequestEvent.build(
message = "Great post!",
relays = listOf("wss://relay.damus.io"),
target = targetEvent,
zapType = LnZapRequestEvent.ZapType.PUBLIC
)
val zapRequest: LnZapRequestEvent = signer.sign(template)NIP-59 — Gift wrap / sealed DM (kind 1059 + 14)
import com.vitorpamplona.quartz.nip17Dm.NIP17Factory
// Creates sealed rumor + gift wrap pair
val (dmEvent, giftWrap) = NIP17Factory.create(
msg = "Private message",
fromSigner = senderSigner,
toUsers = listOf(recipientPubKey),
relayList = listOf("wss://relay.damus.io")
)NIP-23 — Long-form article (kind 30023)
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
val template = LongTextNoteEvent.build(
body = markdownContent,
title = "My Article",
image = "https://example.com/cover.jpg",
summary = "A brief summary",
slug = "my-article" // d-tag
)13. Platform-Specific Notes
JVM / Desktop
// jvmMain dependencies needed in consuming project:
// secp256k1-kmp-jni-jvm and lazysodium-java are transitive from quartz
// But you need JNA on the classpath for libsodium:
implementation("net.java.dev.jna:jna:5.18.1")Android
// androidMain dependencies (transitive from quartz):
// secp256k1-kmp-jni-android, lazysodium-android, jna (aar)
// No extra setup needed beyond the maven dependency.
// For NIP-55 (Android external signer apps):
import com.vitorpamplona.quartz.nip55AndroidSigner.ExternalSignerLauncheriOS
The library produces an XCFramework named quartz-kmpKit.
# Build XCFramework
./gradlew :quartz:assembleQuartz-kmpKitReleaseXCFramework
# Output: quartz/build/XCFrameworks/release/quartz-kmpKit.xcframeworkIn Xcode: drag & drop the .xcframework into your project, then use from Swift via Kotlin/Native interop.
14. Event Store (SQLite, all platforms)
SQLite-backed storage in commonMain (JVM, Android, iOS — uses the bundled
androidx.sqlite driver) with full NIP support (NIP-09, NIP-40, NIP-45, NIP-50,
NIP-62). All operations are suspend:
import com.vitorpamplona.quartz.nip01Core.store.sqlite.EventStore
val store = EventStore() // default DB file "events.db"
// Insert
store.insert(event)
// Query
val events = store.query<Event>(
Filter(authors = listOf(pubKey), kinds = listOf(1), limit = 50)
)
// Count (NIP-45)
val count = store.count(Filter(kinds = listOf(1)))
// Full-text search (NIP-50)
val results = store.query<Event>(Filter(search = "bitcoin"))15. NIP-11 Relay Information Document
If you're standing up a relay on Quartz's relay-server code, serve your NIP-11
document with the type-safe builder — don't hand-write the JSON string.
Package: com.vitorpamplona.quartz.nip11RelayInfo
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
import com.vitorpamplona.quartz.nip11RelayInfo.relayInformation
val info =
relayInformation {
name = "sot"
description = "NIP-50 profile search ranked by Nostr web-of-trust"
software = "https://github.com/vitorpamplona/sot"
version = "0.1"
supports(1, 11, 42, 50) // ints → spec-compliant [1,11,42,50] in the JSON
}
val json = info.toJson() // null/empty fields are omittedServe it at the relay root, branching on the Accept header (Ktor example):
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
import io.ktor.http.ContentType
get("/") {
val accept = call.request.headers[HttpHeaders.Accept].orEmpty()
if (accept.contains(Nip11RelayInformation.CONTENT_TYPE)) { // "application/nostr+json"
call.respondText(json, ContentType.parse(Nip11RelayInformation.CONTENT_TYPE))
} else {
call.respondText("Open a WebSocket (NIP-01) or send Accept: ${Nip11RelayInformation.CONTENT_TYPE}")
}
}Nested objects, lists, and enforced limits
val info =
relayInformation {
name = "Paid Relay"
supports(1, 11, 42)
supportsExtensions("nip50-search") // supported_nip_extensions
countries("US", "CA") // relay_countries; also languages(...), tags(...)
nip50Features("profile_search") // the `nip50` field
// limitation { } — camelCase maps to NIP-11 snake_case fields
limitation {
maxSubscriptions = 20
maxFilters = 10
authRequired = true
}
// fees { } — each helper is repeatable
fees {
admission(amount = 1000, unit = "msats")
publication(amount = 100, unit = "msats", kinds = listOf(1, 30023))
}
// retention(...) — call once per policy entry
retention(kinds = listOf(0, 3), count = 1)
}Keep advertised limits in sync with enforced ones. If you build aRelayLimits for the server's policy chain, hand the same object to the
builder so what you publish can never drift from what you enforce:
import com.vitorpamplona.quartz.nip01Core.relay.server.policies.RelayLimits
val limits = RelayLimits(maxSubscriptions = 20, maxFilters = 10, maxLimit = 500, authRequired = true)
val info =
relayInformation {
name = "My Relay"
supports(1, 11, 42, 45)
limitation(limits) // == limits.toNip11Limitation()
}To load an operator-supplied doc from disk or a string instead of building it,
use Nip11RelayInformation.fromJson(json).
geode(Quartz's standalone relay) builds its default document exactly this
way — seegeode/.../RelayInfo.kt.
16. Quick Reference
| Task | API | Package |
|---|---|---|
| Generate keys | KeyPair() |
nip01Core.crypto |
| Create signer | NostrSignerInternal(keyPair) |
nip01Core.signers |
| Build event | TextNoteEvent.build(...) or Event.build(kind, content) { tags } |
nip10Notes, nip01Core.core |
| Sign event | signer.sign(template) |
nip01Core.signers |
| Serialize | event.toJson() |
nip01Core.core |
| Parse | Event.fromJson(json) |
nip01Core.core |
| ByteArray → hex | bytes.toHexKey() |
nip01Core.core |
| hex → ByteArray | hex.hexToByteArray() / hex.hexToByteArrayOrNull() |
nip01Core.core |
| Validate hex | Hex.isHex(s) / Hex.isHex64(s) / hex.isValid() |
utils, nip01Core.core |
| Now (seconds) | TimeUtils.now() |
utils |
| Relative time | TimeUtils.oneDayAgo() / oneHourAgo() |
utils |
| Secure random | RandomInstance.bytes(n) / randomChars() |
utils |
| Hash / event id | sha256(bytes) / EventHasher.hashId(...) |
utils.sha256, nip01Core.crypto |
| Normalize relay URL | RelayUrlNormalizer.normalize("wss://...") |
nip01Core.relay.normalizer |
| Setup relay client | NostrClient(BasicOkHttpWebSocket.Builder { okhttp }) |
nip01Core.relay.client |
| Subscribe | client.openReqSubscription(subId, mapOf(relay to filters), listener) |
nip01Core.relay.client |
| Publish | client.send(event, setOf(relayUrl)) |
nip01Core.relay.client |
| NIP-44 encrypt | signer.nip44Encrypt(text, recipientPubKey) |
nip01Core.signers |
| Bech32 decode | Nip19Parser.uriToRoute("npub1...") |
nip19Bech32 |
| Bech32 encode | Nip19Bech32.createNPub(pubKeyHex) |
nip19Bech32 |
| Build NIP-11 doc | relayInformation { name = ...; supports(1, 11) } |
nip11RelayInfo |
| Serialize NIP-11 doc | info.toJson() (media type Nip11RelayInformation.CONTENT_TYPE) |
nip11RelayInfo |
Common Event Kinds
| Kind | Event Type | NIP | Quartz class |
|---|---|---|---|
| 0 | User metadata | 01 | MetadataEvent |
| 1 | Text note | 10 | TextNoteEvent |
| 3 | Follow list | 02 | ContactListEvent |
| 4 | Legacy DM | 04 | PrivateDmEvent |
| 5 | Deletion | 09 | DeletionEvent |
| 6 | Repost | 18 | RepostEvent |
| 7 | Reaction | 25 | ReactionEvent |
| 14 | Chat message (sealed) | 17 | NIP17GroupMessage |
| 1059 | Gift wrap | 59 | GiftWrapEvent |
| 9734 | Zap request | 57 | LnZapRequestEvent |
| 9735 | Zap receipt | 57 | LnZapEvent |
| 10002 | Relay list | 65 | AdvertisedRelayListEvent |
| 30023 | Long-form content | 23 | LongTextNoteEvent |
Related Skills
- nostr-expert — Internal Quartz patterns for Amethyst development
- kotlin-multiplatform — KMP source sets, expect/actual patterns
- kotlin-coroutines — Flow patterns for relay event streams