Practical patterns for using Quartz (Amethyst's library) to handle Nostr events, NIPs, and cryptography.

Install

mkdir -p .claude/skills/nostr-expert && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/1749" && unzip -o skill.zip -d .claude/skills/nostr-expert && rm skill.zip

Installs to .claude/skills/nostr-expert

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

Nostr protocol implementation patterns in Quartz (AmethystMultiplatform's KMP Nostr library). Use when working with: (1) Nostr events (creating, parsing, signing), (2) Event kinds and tags, (3) NIP implementations (80+ NIP packages in quartz/), (4) Event builders and TagArrayBuilder DSL, (5) Nostr cryptography (secp256k1, NIP-44 encryption), (6) Relay communication patterns, (7) Bech32 encoding (npub, nsec, note, nevent). Complements nostr-protocol agent (NIP specs) - this skill provides Quartz codebase patterns and implementation details.
545 chars✓ has a “when” triggerlonger than Claude Code's old 250-char listing cap (fine on current versions)
Advanced

Key capabilities

  • Create and sign Nostr events
  • Implement NIP-compliant event types
  • Perform secp256k1 cryptographic operations
  • Encode and decode Bech32 identifiers
  • Resolve user inputs to public keys

How it works

It utilizes the Quartz KMP library to provide DSLs and patterns for event construction, signing, and NIP implementation.

Inputs & outputs

You give it
Nostr event data or identifier
You get back
Signed event or resolved public key

When to use nostr-expert

  • Parse and sign Nostr events
  • Implement a specific NIP
  • Use Bech32 encoding/decoding
  • Communicate with Nostr relays

About this skill

Nostr Protocol Expert (Quartz Implementation)

Practical patterns for working with Nostr in Quartz, AmethystMultiplatform's KMP Nostr library.

When to Use This Skill

  • Implementing Nostr event types (TextNote, Reaction, Zap, etc.)
  • Creating/parsing events with TagArrayBuilder DSL
  • Working with event kinds and tags
  • Finding NIP implementations in quartz/ codebase
  • Nostr cryptography (secp256k1 signing, NIP-44 encryption)
  • Bech32 encoding/decoding (npub, nsec, note formats)
  • Resolving user input (hex / npub / nprofile / NIP-05 name@domain) to a pubkey
  • Event validation and verification

For NIP specifications → Use nostr-protocol agent For Quartz implementation → Use this skill

Quartz Architecture

Quartz organizes code by NIP number:

quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/
├── nip01Core/           # Core protocol (Event, Kind, Tags)
├── nip04Dm/             # Legacy DMs (deprecated)
├── nip10Notes/          # Text notes with threading
├── nip17Dm/             # Private DMs (gift wrap)
├── nip19Bech32/         # Bech32 encoding
├── nip44Encryption/     # Modern encryption (ChaCha20)
├── nip57Zaps/           # Lightning zaps
├── ... (57 NIPs total)
└── experimental/        # Draft NIPs

Pattern: nip##<Name>/ directories contain event classes, tags, and utilities for that NIP.

Find implementations: Use scripts/nip-lookup.sh <nip-number> or see references/nip-catalog.md.

Event Anatomy

Core Structure

@Immutable
open class Event(
    val id: HexKey,              // SHA-256 hash of serialized event
    val pubKey: HexKey,          // Author's public key (32 bytes hex)
    val createdAt: Long,         // Unix timestamp
    val kind: Kind,              // Event kind (Int typealias)
    val tags: TagArray,          // Array of tag arrays
    val content: String,         // Event content
    val sig: HexKey,             // Schnorr signature (64 bytes hex)
) : IEvent

Key insight: Event is the base class. Specific event types (TextNoteEvent, ReactionEvent) extend it and add parsing/helper methods.

Kind Classification

typealias Kind = Int

fun Kind.isEphemeral() = this in 20000..29999      // Not stored
fun Kind.isReplaceable() = this == 0 || this == 3 || this in 10000..19999
fun Kind.isAddressable() = this in 30000..39999    // Replaceable + has d-tag
fun Kind.isRegular() = this in 1000..9999          // Stored, not replaced

Pattern: Kind determines event lifecycle and replaceability.

Creating Events

EventTemplate Pattern

fun eventTemplate(
    kind: Kind,
    content: String,
    tags: TagArray = emptyArray()
): EventTemplate

Usage:

val template = eventTemplate(
    kind = 1,  // Text note
    content = "Hello Nostr!",
    tags = tagArray {
        add(arrayOf("subject", "Greeting"))
    }
)

// Sign with a signer
val signedEvent = signer.sign(template)

Why templates? Separates event data from signing. Templates can be signed by different signers (local keys, remote signers, hardware wallets).

TagArrayBuilder DSL

fun <T : Event> tagArray(
    initializer: TagArrayBuilder<T>.() -> Unit
): TagArray

Methods:

  • add(tag) - Append tag
  • addFirst(tag) - Prepend tag (for ordering)
  • addUnique(tag) - Replace all tags with this name
  • remove(tagName) - Remove by name
  • addAll(tags) - Bulk add

Example:

val tags = tagArray<TextNoteEvent> {
    add(arrayOf("e", replyToEventId, "", "reply"))
    add(arrayOf("p", authorPubkey))
    addUnique(arrayOf("subject", "Re: Hello"))
    add(arrayOf("content-warning", "spoilers"))
}

Pattern: Fluent DSL for building tag arrays with validation and deduplication.

Common Event Types

TextNoteEvent (kind 1)

class TextNoteEvent : BaseThreadedEvent

Creating:

val note = eventTemplate(
    kind = 1,
    content = "Hello world!",
    tags = tagArray {
        add(arrayOf("subject", "First post"))
    }
)

Parsing:

val event: TextNoteEvent = ...
val subject = event.subject()  // Extension from nip14Subject
val mentions = event.mentions()  // List of p-tags
val quotedEvents = event.quotes()  // List of q-tags

ReactionEvent (kind 7)

fun createReaction(
    targetEvent: Event,
    emoji: String = "+"
): EventTemplate {
    return eventTemplate(
        kind = 7,
        content = emoji,
        tags = tagArray {
            add(arrayOf("e", targetEvent.id))
            add(arrayOf("p", targetEvent.pubKey))
        }
    )
}

MetadataEvent (kind 0)

data class UserMetadata(
    val name: String?,
    val displayName: String?,
    val picture: String?,
    val banner: String?,
    val about: String?,
    // ... more fields
)

fun createMetadata(metadata: UserMetadata): EventTemplate {
    return eventTemplate(
        kind = 0,
        content = metadata.toJson()  // Serialize to JSON
    )
}

Addressable Events (kinds 30000-40000)

fun createArticle(
    slug: String,
    title: String,
    content: String
): EventTemplate {
    return eventTemplate(
        kind = 30023,
        content = content,
        tags = tagArray {
            addUnique(arrayOf("d", slug))  // Unique identifier
            add(arrayOf("title", title))
            add(arrayOf("published_at", "${TimeUtils.now()}"))
        }
    )
}

Key: d-tag makes it addressable. Events with same kind + pubkey + d-tag replace each other.

Tag Patterns

Tags are Array<String> with pattern [name, value, ...optionalParams].

Core Tags

e-tag (event reference):

add(arrayOf("e", eventId, relayHint, marker))
// marker: "reply", "root", "mention"

p-tag (pubkey reference):

add(arrayOf("p", pubkey, relayHint))

a-tag (addressable event):

add(arrayOf("a", "$kind:$pubkey:$dtag", relayHint))

d-tag (identifier for addressable events):

addUnique(arrayOf("d", "unique-slug"))

Tag Extensions

// Find tags
event.tags.tagValue("subject")  // First subject tag value
event.tags.allTags("p")  // All p-tags
event.tags.tagValues("e")  // All e-tag values

// Parse structured tags
event.tags.mapNotNull(ETag::parse)  // Parse as ETag objects

For comprehensive tag patterns, see references/tag-patterns.md.

Threading (NIP-10)

fun createReply(
    original: TextNoteEvent,
    content: String
): EventTemplate {
    return eventTemplate(
        kind = 1,
        content = content,
        tags = tagArray {
            // Reply marker
            add(arrayOf("e", original.id, "", "reply"))

            // Root marker (original's root, or original itself)
            original.rootEvent()?.let {
                add(arrayOf("e", it.id, "", "root"))
            } ?: add(arrayOf("e", original.id, "", "root"))

            // Tag author
            add(arrayOf("p", original.pubKey))

            // Tag all mentioned users
            original.mentions().forEach {
                add(arrayOf("p", it))
            }
        }
    )
}

Pattern: reply and root markers establish thread hierarchy.

Cryptography

Signing (secp256k1)

interface ISigner {
    suspend fun sign(template: EventTemplate): Event
}

// Local key signing
class LocalSigner(private val privateKey: ByteArray) : ISigner {
    override suspend fun sign(template: EventTemplate): Event {
        val id = template.generateId()
        val sig = Secp256k1.sign(id, privateKey)
        return Event(id, pubKey, createdAt, kind, tags, content, sig)
    }
}

Pattern: Signers abstract key management. Can be local, remote (NIP-46), or hardware.

Encryption (NIP-44)

// Modern encryption (ChaCha20-Poly1305) via the Nip44 facade
// (nip44Encryption/Nip44.kt — picks the current version, decrypts any)
object Nip44 {
    fun encrypt(msg: String, privateKey: ByteArray, pubKey: ByteArray): Nip44v2.EncryptedInfo
    fun decrypt(payload: String, privateKey: ByteArray, pubKey: ByteArray): String
}

// Usage
val encrypted = Nip44.encrypt("Secret message", myPrivateKey, recipientPubKey)
val payload = encrypted.encodePayload()  // base64 string for event content

val decrypted = Nip44.decrypt(payload, myPrivateKey, senderPubKey)

Most code should not call Nip44 directly — go through signer.nip44Encrypt(plaintext, toPublicKey) / signer.nip44Decrypt(ciphertext, fromPublicKey) so remote/external signers keep working.

Pattern: Elliptic curve Diffie-Hellman + ChaCha20-Poly1305 AEAD.

NIP-04 (Deprecated)

// Legacy encryption (NIP-04, deprecated for NIP-44)
object Nip04 {
    fun encrypt(msg: String, privateKey: ByteArray, pubKey: HexKey): String
    fun decrypt(msg: String, privateKey: ByteArray, pubKey: HexKey): String
}

Note: Use NIP-44 (Nip44) for new implementations. NIP-04 has security issues.

Hex Encoding (HexKey ↔ ByteArray)

Pubkeys, event ids and signatures are lower-case hex. Quartz uses the HexKey typealias (= String) plus extensions in nip01Core/core/HexKey.kt, backed by the Hex object in utils/Hex.kt. Use these — never hand-roll a byte loop or import a third-party hex codec.

import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull
import com.vitorpamplona.quartz.nip01Core.core.isValid
import com.vitorpamplona.quartz.utils.Hex

val hex: HexKey = bytes.toHexKey()            // ByteArray -> lower-case hex
val back: ByteArray = hex.hexToByteArray()    // hex -> ByteArray (throws on odd length)
val safe: ByteArray? = input.hexToByteArrayOrNull()  // null on invalid hex

Hex.isHex(input)     // valid hex, any length
Hex.isHex64(input)   // ~30% faster fast-path for a 32-byte key/id
hex.isValid()        // 64 chars + valid hex (pubkey / 

---

*Content truncated.*

When not to use it

  • Non-Quartz Nostr implementations

Prerequisites

Quartz library

Limitations

  • Limited to Quartz framework patterns
  • Requires understanding of NIP specifications

How it compares

It provides specific Quartz codebase patterns and implementation details rather than general protocol theory.

Compared to similar skills

nostr-expert side by side with the closest alternatives in the catalog.

SkillInstallsUpdatedSafetyDifficulty
nostr-expert (this skill)32moReviewAdvanced
android-kotlin-development2685moReviewAdvanced
mqtt-kmp03moReviewAdvanced
kotlin-ktor03moNo flagsIntermediate

Try saying

Example prompts that trigger this skill in your AI assistant.

More by vitorpamplona

View all by vitorpamplona

kotlin-multiplatform

vitorpamplona

Platform abstraction decision-making for Amethyst KMP project. Guides when to abstract vs keep platform-specific, source set placement (commonMain, jvmAndroid, platform-specific), expect/actual patterns. Covers primary targets (Android, JVM/Desktop, iOS) with web/wasm future considerations. Integrates with gradle-expert for dependency issues. Triggers on: abstraction decisions ("should I share this?"), source set placement questions, expect/actual creation, build.gradle.kts work, incorrect placement detection, KMP dependency suggestions.

32156

compose-expert

vitorpamplona

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 icons, or determining whether to share UI in commonMain vs keep platform-specific. Delegates navigation to android-expert/desktop-expert. Complements kotlin-expert (handles Kotlin language aspects of state/annotations).

632

kotlin-expert

vitorpamplona

Advanced Kotlin patterns for AmethystMultiplatform. Flow state management (StateFlow/SharedFlow), sealed hierarchies (classes vs interfaces), immutability (@Immutable, data classes), DSL builders (type-safe fluent APIs), inline functions (reified generics, performance). Use when working with: (1) State management patterns (StateFlow/SharedFlow/MutableStateFlow), (2) Sealed classes or sealed interfaces, (3) @Immutable annotations for Compose, (4) DSL builders with lambda receivers, (5) inline/reified functions, (6) Kotlin performance optimization. Complements kotlin-coroutines agent (async patterns) - this skill focuses on Amethyst-specific Kotlin idioms.

526

kotlin-coroutines

vitorpamplona

Advanced Kotlin coroutines patterns for AmethystMultiplatform. Use when working with: (1) Structured concurrency (supervisorScope, coroutineScope), (2) Advanced Flow operators (flatMapLatest, combine, merge, shareIn, stateIn), (3) Channels and callbackFlow, (4) Dispatcher management and context switching, (5) Exception handling (CoroutineExceptionHandler, SupervisorJob), (6) Testing async code (runTest, Turbine), (7) Nostr relay connection pools and subscriptions, (8) Backpressure handling in event streams. Delegates to kotlin-expert for basic StateFlow/SharedFlow patterns. Complements nostr-expert for relay communication.

311

gradle-expert

vitorpamplona

Build optimization, dependency resolution, and multi-module KMP troubleshooting for AmethystMultiplatform. Use when working with: (1) Gradle build files (build.gradle.kts, settings.gradle), (2) Version catalog (libs.versions.toml), (3) Build errors and dependency conflicts, (4) Module dependencies and source sets, (5) Desktop packaging (DMG/MSI/DEB), (6) Build performance optimization, (7) Proguard/R8 configuration, (8) Common KMP + Android Gradle issues (Compose conflicts, secp256k1 JNI variants, source set problems).

19

You might also like

android-kotlin-development

aj-geddes

Develop native Android apps with Kotlin. Covers MVVM with Jetpack, Compose for modern UI, Retrofit for API calls, Room for local storage, and navigation architecture.

268679

mqtt-kmp

meshtastic

MQTTastic Client KMP — Kotlin Multiplatform MQTT 5.0 client library project knowledge

00

kotlin-ktor

navikt

Ktor-applikasjoner i Kotlin — routes, plugins, Koin DI, JWT-claims som NAVident, CallLogging/MDC, StatusPages/ApiError, paginering og input-validering. Brukes via /kotlin-ktor ved Ktor-arbeid.

00

vertigram-telegram-api-bump

gagarski

Update Vertigram's vertigram-telegram-client module for a requested Telegram Bot API version. Use when the user asks to bump supported Telegram Bot API versions, apply Telegram Bot API changelog entries, add Telegram API types/methods/json body definitions, update Telegram markup builders, or add se

00

kafka

navikt

Rapids & Rivers, eventdrevet arkitektur, Kafka-mønstre og schema-design for Nav-applikasjoner

00

kotlin-multiplatform

vitorpamplona

Platform abstraction decision-making for Amethyst KMP project. Guides when to abstract vs keep platform-specific, source set placement (commonMain, jvmAndroid, platform-specific), expect/actual patterns. Covers primary targets (Android, JVM/Desktop, iOS) with web/wasm future considerations. Integrates with gradle-expert for dependency issues. Triggers on: abstraction decisions ("should I share this?"), source set placement questions, expect/actual creation, build.gradle.kts work, incorrect placement detection, KMP dependency suggestions.

32156

Search skills

Search the agent skills registry