nostr-expert
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.zipInstalls 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.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
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 tagaddFirst(tag)- Prepend tag (for ordering)addUnique(tag)- Replace all tags with this nameremove(tagName)- Remove by nameaddAll(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
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.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| nostr-expert (this skill) | 3 | 2mo | Review | Advanced |
| android-kotlin-development | 268 | 5mo | Review | Advanced |
| mqtt-kmp | 0 | 3mo | Review | Advanced |
| kotlin-ktor | 0 | 3mo | No flags | Intermediate |
Try saying
Example prompts that trigger this skill in your AI assistant.
More by vitorpamplona
View all by vitorpamplona →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.
mqtt-kmp
meshtastic
MQTTastic Client KMP — Kotlin Multiplatform MQTT 5.0 client library project knowledge
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.
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
kafka
navikt
Rapids & Rivers, eventdrevet arkitektur, Kafka-mønstre og schema-design for Nav-applikasjoner
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.