/quartz-integration
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
$ npx -y skills add vitorpamplona/amethyst --skill quartz-integration --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/quartz-integration
Context preview
The summary Claude sees to decide when to auto-load this skill.
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
SKILL.md
quartz-integration.SKILL.mdname: quartz-integration
description: 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).
Quartz Integration Guide
Reference for integrating `com.vitorpamplona.quartz:quartz` into external Nostr KMP projects.
**Published artifact**: `com.vitorpamplona.quartz:quartz:1.13.1` (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.13.1"
[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.13.1")
}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..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)
| 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 | | i
Read more
name: quartz-integration description: 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).
Quartz Integration Guide
Reference for integrating `com.vitorpamplona.quartz:quartz` into external Nostr KMP projects.
**Published artifact**: `com.vitorpamplona.quartz:quartz:1.13.1` (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.13.1"
[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.13.1")
}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..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)
| 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 | | i
Other skills on amethyst.
- /account-state
Account state and in-memory event store patterns in Amethyst. Use when working with `Account.kt` (per-user state objects — `kind3FollowList`, `nip65RelayList`, `muteList`, `bookmarkState`, each exposing a `.flow` StateFlow), `LocalCache` (the object-level event store backed by
Open skill - /amy-expert
Patterns for extending `amy`, the Amethyst CLI in `cli/`. Use when adding an `amy <verb>` command, touching files under `cli/src/main/kotlin/…/cli/`, wiring a new subcommand into `Main.kt`, writing an interop test script that drives Amy, or extracting logic out of `amethyst/`
Open skill - /android-expert
Android platform patterns for the `amethyst/` module. Use when working with (1) Android navigation (Navigation Compose, type-safe routes, bottom nav), (2) runtime permissions (camera, notifications, biometrics), (3) platform APIs (Intent, Context, Activity, ContentResolver), (4)
Open skill - /auth-signers
Signer abstraction patterns in Amethyst. Use when working with event signing, choosing between a local keypair (`NostrSignerInternal`), a remote NIP-46 bunker signer (`NostrSignerRemote`), or a NIP-55 Android external-app signer (`NostrSignerExternal`). Covers the abstract
Open skill - /compose-expert
Advanced Compose Multiplatform UI patterns for shared composables. Use when working with visual UI components, state management patterns (remember, derivedStateOf, produceState), recomposition optimization (@Stable/@Immutable visual usage), Material3 theming, custom ImageVector
Open skill - /compose-modifier-and-layout-style
Use when writing or reviewing Jetpack Compose layout APIs, modifier parameters, modifier chain construction, hardcoded root layout decisions, or layout wrappers around a single conditional. Technique-layer skill — complements the codebase-specific compose-expert.
Open skill

