quartz-integration

v2026.09.24

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).

GitHub
安装命令
npx skhub add vitorpamplona/quartz-integration
Markdown
SKILL.md

Quartz Integration Guide

Reference for integrating com.vitorpamplona.quartz:quartz into external Nostr KMP projects.

Published artifact: com.vitorpamplona.quartz:quartz:1.16.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.16.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.16.0")
}

Transitive dependencies pulled in automatically

Quartz exposes these as api (you get them transitively):

DependencyUsed for
fr.acinq.secp256k1:secp256k1-kmp-*Schnorr signing
com.github.anthonynsimon:rfc3986-normalizerRelay URL normalization
com.fasterxml.jackson.module:jackson-module-kotlinEvent 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..29999

3. 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) and com.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)
NeedCallNotes
ByteArray → hexbytes.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 same nip01Core.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 id

Hashing — 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-level Bech32 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 prefix

Base64. 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 use Base64.Default (standard, padded); url-safe contexts use Base64.UrlSafe (configure padding via .withPadding(...)).

NeedCall
Now (event created_at)TimeUtils.now() (seconds)
Relative filter boundTimeUtils.oneDayAgo() / oneHourAgo() / …
Secure random bytesRandomInstance.bytes(n)
Subscription idRandomInstance.randomChars()
Raw hashsha256(bytes)
Event id / verifyEventHasher.hashId(...) / hashIdCheck(...)
Bech32 custom prefixBech32.encodeBytes(hrp, bytes, enc) / s.bechToBytes(hrp)
Base64kotlin.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 name

6. 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? TextNoteEvent

10. 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.ExternalSignerLauncher

iOS

The library produces an XCFramework named quartz-kmpKit.

# Build XCFramework
./gradlew :quartz:assembleQuartz-kmpKitReleaseXCFramework
# Output: quartz/build/XCFrameworks/release/quartz-kmpKit.xcframework

In 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 omitted

Serve 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 a RelayLimits 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 — see geode/.../RelayInfo.kt.


16. Quick Reference

TaskAPIPackage
Generate keysKeyPair()nip01Core.crypto
Create signerNostrSignerInternal(keyPair)nip01Core.signers
Build eventTextNoteEvent.build(...) or Event.build(kind, content) { tags }nip10Notes, nip01Core.core
Sign eventsigner.sign(template)nip01Core.signers
Serializeevent.toJson()nip01Core.core
ParseEvent.fromJson(json)nip01Core.core
ByteArray → hexbytes.toHexKey()nip01Core.core
hex → ByteArrayhex.hexToByteArray() / hex.hexToByteArrayOrNull()nip01Core.core
Validate hexHex.isHex(s) / Hex.isHex64(s) / hex.isValid()utils, nip01Core.core
Now (seconds)TimeUtils.now()utils
Relative timeTimeUtils.oneDayAgo() / oneHourAgo()utils
Secure randomRandomInstance.bytes(n) / randomChars()utils
Hash / event idsha256(bytes) / EventHasher.hashId(...)utils.sha256, nip01Core.crypto
Normalize relay URLRelayUrlNormalizer.normalize("wss://...")nip01Core.relay.normalizer
Setup relay clientNostrClient(BasicOkHttpWebSocket.Builder { okhttp })nip01Core.relay.client
Subscribeclient.openReqSubscription(subId, mapOf(relay to filters), listener)nip01Core.relay.client
Publishclient.send(event, setOf(relayUrl))nip01Core.relay.client
NIP-44 encryptsigner.nip44Encrypt(text, recipientPubKey)nip01Core.signers
Bech32 decodeNip19Parser.uriToRoute("npub1...")nip19Bech32
Bech32 encodeNip19Bech32.createNPub(pubKeyHex)nip19Bech32
Build NIP-11 docrelayInformation { name = ...; supports(1, 11) }nip11RelayInfo
Serialize NIP-11 docinfo.toJson() (media type Nip11RelayInformation.CONTENT_TYPE)nip11RelayInfo

Common Event Kinds

KindEvent TypeNIPQuartz class
0User metadata01MetadataEvent
1Text note10TextNoteEvent
3Follow list02ContactListEvent
4Legacy DM04PrivateDmEvent
5Deletion09DeletionEvent
6Repost18RepostEvent
7Reaction25ReactionEvent
14Chat message (sealed)17NIP17GroupMessage
1059Gift wrap59GiftWrapEvent
9734Zap request57LnZapRequestEvent
9735Zap receipt57LnZapEvent
10002Relay list65AdvertisedRelayListEvent
30023Long-form content23LongTextNoteEvent

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
发现
标签

此技能尚未发布标签。

版本
最新版本元数据

版本

v2026.09.24

发布时间

2026年9月24日

分类

未分类

许可证

MIT

源路径

.claude/skills/quartz-integration

默认分支

main

最新提交

3a577f0

Tree SHA

62dda9d