diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt
index 501c24ae0..375f0dd12 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt
@@ -229,6 +229,8 @@ import com.vitorpamplona.quartz.nip64Chess.LiveChessGameChallengeEvent
import com.vitorpamplona.quartz.nip64Chess.LiveChessGameEndEvent
import com.vitorpamplona.quartz.nip64Chess.LiveChessMoveEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
+import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.RelayDiscoveryEvent
+import com.vitorpamplona.quartz.nip66RelayMonitor.monitor.RelayMonitorEvent
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent
import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent
@@ -582,6 +584,8 @@ private fun kindDisplayName(kind: Int): Int =
ReactionEvent.KIND -> R.string.kind_reactions
ContactCardEvent.KIND -> R.string.kind_contact_card
RelayAuthEvent.KIND -> R.string.kind_relay_auth
+ RelayDiscoveryEvent.KIND -> R.string.kind_relay_discovery
+ RelayMonitorEvent.KIND -> R.string.kind_relay_monitor
RelaySetEvent.KIND -> R.string.kind_relay_set
ReportEvent.KIND -> R.string.kind_reports
RepostEvent.KIND -> R.string.kind_reposts
diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml
index b115e2706..771a576c6 100644
--- a/amethyst/src/main/res/values/strings.xml
+++ b/amethyst/src/main/res/values/strings.xml
@@ -1663,6 +1663,8 @@
Reactions
Contact Card
Relay Auth
+ Relay Discovery
+ Relay Monitor Announcement
Relay Set
Reports
Reposts
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/RelayDiscoveryEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/RelayDiscoveryEvent.kt
new file mode 100644
index 000000000..560195443
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/RelayDiscoveryEvent.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2025 Vitor Pamplona
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.vitorpamplona.quartz.nip66RelayMonitor.discovery
+
+import androidx.compose.runtime.Immutable
+import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
+import com.vitorpamplona.quartz.nip01Core.core.HexKey
+import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
+import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
+import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
+import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
+import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
+import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashes
+import com.vitorpamplona.quartz.nip31Alts.alt
+import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.RttType
+import com.vitorpamplona.quartz.utils.TimeUtils
+
+/**
+ * NIP-66 Kind 30166: Relay Discovery Event
+ *
+ * Published by relay monitors to document the state of a specific relay.
+ * The `d` tag holds the normalized relay URL, making each relay URL
+ * addressable per monitor pubkey.
+ *
+ * Carries metrics and metadata observed during monitoring:
+ * - Round-trip times (rtt-open, rtt-read, rtt-write)
+ * - Network type (n), relay type (T)
+ * - Supported NIPs (N), accepted kinds (k)
+ * - Requirements (R: auth, payment, pow, writes)
+ * - Topics (t) and geohash (g)
+ */
+@Immutable
+class RelayDiscoveryEvent(
+ id: HexKey,
+ pubKey: HexKey,
+ createdAt: Long,
+ tags: Array>,
+ content: String,
+ sig: HexKey,
+) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
+ fun relay(): NormalizedRelayUrl? = RelayUrlNormalizer.normalizeOrNull(dTag())
+
+ fun rtt() = tags.rtts()
+
+ fun rttOpen() = tags.rtt(RttType.OPEN)
+
+ fun rttRead() = tags.rtt(RttType.READ)
+
+ fun rttWrite() = tags.rtt(RttType.WRITE)
+
+ fun networkTypes() = tags.networkTypes()
+
+ fun relayTypes() = tags.relayTypes()
+
+ fun supportedNips() = tags.supportedNips()
+
+ fun requirements() = tags.requirements()
+
+ fun acceptedKinds() = tags.acceptedKinds()
+
+ fun topics() = tags.topics()
+
+ fun geohashes() = tags.geohashes()
+
+ companion object {
+ const val KIND = 30166
+ const val ALT_DESCRIPTION = "Relay discovery"
+
+ fun build(
+ relayUrl: NormalizedRelayUrl,
+ content: String = "",
+ createdAt: Long = TimeUtils.now(),
+ initializer: TagArrayBuilder.() -> Unit = {},
+ ) = eventTemplate(KIND, content, createdAt) {
+ alt(ALT_DESCRIPTION)
+ dTag(relayUrl.url)
+ initializer()
+ }
+
+ fun build(
+ relayUrl: String,
+ content: String = "",
+ createdAt: Long = TimeUtils.now(),
+ initializer: TagArrayBuilder.() -> Unit = {},
+ ) = build(
+ relayUrl = RelayUrlNormalizer.normalize(relayUrl),
+ content = content,
+ createdAt = createdAt,
+ initializer = initializer,
+ )
+ }
+}
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/TagArrayBuilderExt.kt
new file mode 100644
index 000000000..73234032d
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/TagArrayBuilderExt.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2025 Vitor Pamplona
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.vitorpamplona.quartz.nip66RelayMonitor.discovery
+
+import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
+import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.AcceptedKindTag
+import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.NetworkType
+import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.NetworkTypeTag
+import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.RelayTypeTag
+import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.RequirementTag
+import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.RttTag
+import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.RttType
+import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.SupportedNipTag
+
+fun TagArrayBuilder.rtt(
+ type: RttType,
+ milliseconds: Long,
+) = addUnique(RttTag.assemble(type, milliseconds))
+
+fun TagArrayBuilder.networkType(network: NetworkType) = addUnique(NetworkTypeTag.assemble(network))
+
+fun TagArrayBuilder.networkTypes(networks: List) = addAll(NetworkTypeTag.assemble(networks))
+
+fun TagArrayBuilder.relayType(type: String) = addUnique(RelayTypeTag.assemble(type))
+
+fun TagArrayBuilder.relayTypes(types: List) = addAll(RelayTypeTag.assemble(types))
+
+fun TagArrayBuilder.supportedNip(nip: Int) = add(SupportedNipTag.assemble(nip))
+
+fun TagArrayBuilder.supportedNips(nips: List) = addAll(SupportedNipTag.assemble(nips))
+
+fun TagArrayBuilder.requirement(
+ value: String,
+ negated: Boolean = false,
+) = add(RequirementTag.assemble(value, negated))
+
+fun TagArrayBuilder.requirements(reqs: List) = addAll(RequirementTag.assemble(reqs))
+
+fun TagArrayBuilder.acceptedKind(
+ kind: Int,
+ negated: Boolean = false,
+) = add(AcceptedKindTag.assemble(kind, negated))
+
+fun TagArrayBuilder.acceptedKinds(kinds: List) = addAll(AcceptedKindTag.assemble(kinds))
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/TagArrayExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/TagArrayExt.kt
new file mode 100644
index 000000000..a2944d40b
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/TagArrayExt.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2025 Vitor Pamplona
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.vitorpamplona.quartz.nip66RelayMonitor.discovery
+
+import com.vitorpamplona.quartz.nip01Core.core.TagArray
+import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
+import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.AcceptedKindTag
+import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.NetworkTypeTag
+import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.RelayTypeTag
+import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.RequirementTag
+import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.RttTag
+import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.RttType
+import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.SupportedNipTag
+
+fun TagArray.rtts() = mapNotNull(RttTag::parse)
+
+fun TagArray.rtt(type: RttType) = mapNotNull(RttTag::parse).firstOrNull { it.type == type }?.milliseconds
+
+fun TagArray.networkTypes() = mapNotNull(NetworkTypeTag::parse)
+
+fun TagArray.relayTypes() = mapNotNull(RelayTypeTag::parse)
+
+fun TagArray.supportedNips() = mapNotNull(SupportedNipTag::parse)
+
+fun TagArray.requirements() = mapNotNull(RequirementTag::parse)
+
+fun TagArray.acceptedKinds() = mapNotNull(AcceptedKindTag::parse)
+
+fun TagArray.topics() = hashtags()
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/AcceptedKindTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/AcceptedKindTag.kt
new file mode 100644
index 000000000..a19e1d93e
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/AcceptedKindTag.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2025 Vitor Pamplona
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags
+
+import androidx.compose.runtime.Stable
+import com.vitorpamplona.quartz.nip01Core.core.has
+import com.vitorpamplona.quartz.utils.ensure
+
+/**
+ * ["k", ""] - an event kind accepted by this relay.
+ *
+ * A leading "!" negates, meaning the relay does NOT accept that kind.
+ * Examples: "1" (accepts kind 1), "!1" (rejects kind 1)
+ */
+@Stable
+class AcceptedKindTag(
+ val kind: Int,
+ val negated: Boolean,
+) {
+ companion object {
+ const val TAG_NAME = "k"
+ private const val NEGATION_PREFIX = "!"
+
+ fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME
+
+ fun parse(tag: Array): AcceptedKindTag? {
+ ensure(tag.has(1)) { return null }
+ ensure(tag[0] == TAG_NAME) { return null }
+ ensure(tag[1].isNotEmpty()) { return null }
+ val raw = tag[1]
+ return if (raw.startsWith(NEGATION_PREFIX)) {
+ val kind = raw.removePrefix(NEGATION_PREFIX).toIntOrNull() ?: return null
+ AcceptedKindTag(kind = kind, negated = true)
+ } else {
+ val kind = raw.toIntOrNull() ?: return null
+ AcceptedKindTag(kind = kind, negated = false)
+ }
+ }
+
+ fun assemble(
+ kind: Int,
+ negated: Boolean = false,
+ ) = arrayOf(TAG_NAME, if (negated) "$NEGATION_PREFIX$kind" else kind.toString())
+
+ fun assemble(entry: AcceptedKindTag) = assemble(entry.kind, entry.negated)
+
+ fun assemble(kinds: List) = kinds.map { assemble(it) }
+ }
+}
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/NetworkTypeTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/NetworkTypeTag.kt
new file mode 100644
index 000000000..91d76e68e
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/NetworkTypeTag.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2025 Vitor Pamplona
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags
+
+import com.vitorpamplona.quartz.nip01Core.core.has
+import com.vitorpamplona.quartz.utils.ensure
+
+/** Network accessibility types for relay connections */
+enum class NetworkType(
+ val code: String,
+) {
+ CLEARNET("clearnet"),
+ TOR("tor"),
+ I2P("i2p"),
+ LOKI("loki"),
+}
+
+/** ["n", ""] - the network type the relay is accessible on */
+class NetworkTypeTag {
+ companion object {
+ const val TAG_NAME = "n"
+
+ fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME
+
+ fun parse(tag: Array): NetworkType? {
+ ensure(tag.has(1)) { return null }
+ ensure(tag[0] == TAG_NAME) { return null }
+ ensure(tag[1].isNotEmpty()) { return null }
+ return NetworkType.entries.find { it.code == tag[1] }
+ }
+
+ fun assemble(network: NetworkType) = arrayOf(TAG_NAME, network.code)
+
+ fun assemble(networks: List) = networks.map { assemble(it) }
+ }
+}
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/RelayTypeTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/RelayTypeTag.kt
new file mode 100644
index 000000000..2e6f9b7ff
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/RelayTypeTag.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2025 Vitor Pamplona
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags
+
+import com.vitorpamplona.quartz.nip01Core.core.has
+import com.vitorpamplona.quartz.utils.ensure
+
+/**
+ * ["T", ""] - the type of relay in PascalCase (e.g. PrivateInbox, PublicOutbox).
+ * Values are arbitrary strings defined by relay operators; not an enum.
+ */
+class RelayTypeTag {
+ companion object {
+ const val TAG_NAME = "T"
+
+ fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME
+
+ fun parse(tag: Array): String? {
+ ensure(tag.has(1)) { return null }
+ ensure(tag[0] == TAG_NAME) { return null }
+ ensure(tag[1].isNotEmpty()) { return null }
+ return tag[1]
+ }
+
+ fun assemble(relayType: String) = arrayOf(TAG_NAME, relayType)
+
+ fun assemble(relayTypes: List) = relayTypes.map { assemble(it) }
+ }
+}
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/RequirementTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/RequirementTag.kt
new file mode 100644
index 000000000..4815bccbd
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/RequirementTag.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2025 Vitor Pamplona
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags
+
+import androidx.compose.runtime.Stable
+import com.vitorpamplona.quartz.nip01Core.core.has
+import com.vitorpamplona.quartz.utils.ensure
+
+/**
+ * ["R", ""] - access requirement enforced by the relay.
+ *
+ * A leading "!" negates the requirement, meaning the relay does NOT enforce it.
+ * Examples: "auth", "payment", "!payment" (no payment required), "pow", "writes"
+ */
+@Stable
+class RequirementTag(
+ val value: String,
+ val negated: Boolean,
+) {
+ companion object {
+ const val TAG_NAME = "R"
+ private const val NEGATION_PREFIX = "!"
+
+ fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME
+
+ fun parse(tag: Array): RequirementTag? {
+ ensure(tag.has(1)) { return null }
+ ensure(tag[0] == TAG_NAME) { return null }
+ ensure(tag[1].isNotEmpty()) { return null }
+ val raw = tag[1]
+ return if (raw.startsWith(NEGATION_PREFIX)) {
+ RequirementTag(value = raw.removePrefix(NEGATION_PREFIX), negated = true)
+ } else {
+ RequirementTag(value = raw, negated = false)
+ }
+ }
+
+ fun assemble(
+ value: String,
+ negated: Boolean = false,
+ ) = arrayOf(TAG_NAME, if (negated) "$NEGATION_PREFIX$value" else value)
+
+ fun assemble(req: RequirementTag) = assemble(req.value, req.negated)
+
+ fun assemble(requirements: List) = requirements.map { assemble(it) }
+ }
+}
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/RttTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/RttTag.kt
new file mode 100644
index 000000000..534d2ab48
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/RttTag.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2025 Vitor Pamplona
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags
+
+import androidx.compose.runtime.Stable
+import com.vitorpamplona.quartz.nip01Core.core.has
+import com.vitorpamplona.quartz.utils.ensure
+
+/** Round-trip time measurement types */
+enum class RttType(
+ val tagName: String,
+) {
+ OPEN("rtt-open"),
+ READ("rtt-read"),
+ WRITE("rtt-write"),
+}
+
+/**
+ * Round-trip time tags measured in milliseconds.
+ *
+ * ["rtt-open", ""]
+ * ["rtt-read", ""]
+ * ["rtt-write", ""]
+ */
+@Stable
+class RttTag(
+ val type: RttType,
+ val milliseconds: Long,
+) {
+ companion object {
+ fun isTag(tag: Array) = tag.has(1) && RttType.entries.any { it.tagName == tag[0] }
+
+ fun parse(tag: Array): RttTag? {
+ ensure(tag.has(1)) { return null }
+ val type = RttType.entries.find { it.tagName == tag[0] } ?: return null
+ val ms = tag[1].toLongOrNull() ?: return null
+ return RttTag(type, ms)
+ }
+
+ fun assemble(
+ type: RttType,
+ milliseconds: Long,
+ ) = arrayOf(type.tagName, milliseconds.toString())
+
+ fun assemble(rtt: RttTag) = assemble(rtt.type, rtt.milliseconds)
+ }
+}
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/SupportedNipTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/SupportedNipTag.kt
new file mode 100644
index 000000000..6cfe3ba87
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/SupportedNipTag.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2025 Vitor Pamplona
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags
+
+import com.vitorpamplona.quartz.nip01Core.core.has
+import com.vitorpamplona.quartz.utils.ensure
+
+/** ["N", ""] - a NIP number supported by this relay (repeated per supported NIP) */
+class SupportedNipTag {
+ companion object {
+ const val TAG_NAME = "N"
+
+ fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME
+
+ fun parse(tag: Array): Int? {
+ ensure(tag.has(1)) { return null }
+ ensure(tag[0] == TAG_NAME) { return null }
+ ensure(tag[1].isNotEmpty()) { return null }
+ return tag[1].toIntOrNull()
+ }
+
+ fun assemble(nip: Int) = arrayOf(TAG_NAME, nip.toString())
+
+ fun assemble(nips: List) = nips.map { assemble(it) }
+ }
+}
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/RelayMonitorEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/RelayMonitorEvent.kt
new file mode 100644
index 000000000..a84925eb3
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/RelayMonitorEvent.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2025 Vitor Pamplona
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.vitorpamplona.quartz.nip66RelayMonitor.monitor
+
+import androidx.compose.runtime.Immutable
+import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent
+import com.vitorpamplona.quartz.nip01Core.core.HexKey
+import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
+import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
+import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash
+import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashes
+import com.vitorpamplona.quartz.nip31Alts.alt
+import com.vitorpamplona.quartz.nip66RelayMonitor.monitor.tags.CheckType
+import com.vitorpamplona.quartz.nip66RelayMonitor.monitor.tags.TimeoutTag
+import com.vitorpamplona.quartz.utils.TimeUtils
+
+/**
+ * NIP-66 Kind 10166: Relay Monitor Announcement
+ *
+ * Published by monitoring services to advertise themselves. Declares:
+ * - How frequently the monitor publishes results (frequency tag, required)
+ * - Which types of checks it performs (c tags)
+ * - Timeout values used for each check type (timeout tags)
+ * - Geographical location of the monitor (g tags)
+ *
+ * Monitors should also publish complementary kind 0 (metadata) and
+ * kind 10002 (relay list) events for full discoverability.
+ */
+@Immutable
+class RelayMonitorEvent(
+ id: HexKey,
+ pubKey: HexKey,
+ createdAt: Long,
+ tags: Array>,
+ content: String,
+ sig: HexKey,
+) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
+ fun frequency() = tags.frequency()
+
+ fun checks() = tags.checks()
+
+ fun timeouts() = tags.timeouts()
+
+ fun geohashes() = tags.geohashes()
+
+ companion object {
+ const val KIND = 10166
+ const val ALT_DESCRIPTION = "Relay monitor announcement"
+
+ fun build(
+ frequencySeconds: Long,
+ checks: List = emptyList(),
+ timeouts: List = emptyList(),
+ geohash: String? = null,
+ createdAt: Long = TimeUtils.now(),
+ initializer: TagArrayBuilder.() -> Unit = {},
+ ) = eventTemplate(KIND, "", createdAt) {
+ alt(ALT_DESCRIPTION)
+ frequency(frequencySeconds)
+ checks(checks)
+ timeouts(timeouts)
+ geohash?.let { geohash(it) }
+ initializer()
+ }
+ }
+}
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/TagArrayBuilderExt.kt
new file mode 100644
index 000000000..ba2d64568
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/TagArrayBuilderExt.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2025 Vitor Pamplona
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.vitorpamplona.quartz.nip66RelayMonitor.monitor
+
+import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
+import com.vitorpamplona.quartz.nip66RelayMonitor.monitor.tags.CheckTag
+import com.vitorpamplona.quartz.nip66RelayMonitor.monitor.tags.CheckType
+import com.vitorpamplona.quartz.nip66RelayMonitor.monitor.tags.FrequencyTag
+import com.vitorpamplona.quartz.nip66RelayMonitor.monitor.tags.TimeoutTag
+
+fun TagArrayBuilder.frequency(seconds: Long) = addUnique(FrequencyTag.assemble(seconds))
+
+fun TagArrayBuilder.checks(checks: List) = addAll(CheckTag.assemble(checks))
+
+fun TagArrayBuilder.timeouts(timeouts: List) = addAll(TimeoutTag.assemble(timeouts))
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/TagArrayExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/TagArrayExt.kt
new file mode 100644
index 000000000..ee507b330
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/TagArrayExt.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2025 Vitor Pamplona
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.vitorpamplona.quartz.nip66RelayMonitor.monitor
+
+import com.vitorpamplona.quartz.nip01Core.core.TagArray
+import com.vitorpamplona.quartz.nip66RelayMonitor.monitor.tags.CheckTag
+import com.vitorpamplona.quartz.nip66RelayMonitor.monitor.tags.FrequencyTag
+import com.vitorpamplona.quartz.nip66RelayMonitor.monitor.tags.TimeoutTag
+
+fun TagArray.frequency() = firstNotNullOfOrNull(FrequencyTag::parse)
+
+fun TagArray.checks() = mapNotNull(CheckTag::parse)
+
+fun TagArray.timeouts() = mapNotNull(TimeoutTag::parse)
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/tags/CheckTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/tags/CheckTag.kt
new file mode 100644
index 000000000..2db69a314
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/tags/CheckTag.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2025 Vitor Pamplona
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.vitorpamplona.quartz.nip66RelayMonitor.monitor.tags
+
+import com.vitorpamplona.quartz.nip01Core.core.has
+import com.vitorpamplona.quartz.utils.ensure
+
+/** Types of checks a relay monitor can perform */
+enum class CheckType(
+ val code: String,
+) {
+ OPEN("open"),
+ READ("read"),
+ WRITE("write"),
+ AUTH("auth"),
+ NIP11("nip11"),
+ DNS("dns"),
+ GEO("geo"),
+}
+
+/** ["c", ""] - a capability check conducted by this monitor */
+class CheckTag {
+ companion object {
+ const val TAG_NAME = "c"
+
+ fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME
+
+ fun parse(tag: Array): CheckType? {
+ ensure(tag.has(1)) { return null }
+ ensure(tag[0] == TAG_NAME) { return null }
+ ensure(tag[1].isNotEmpty()) { return null }
+ return CheckType.entries.find { it.code == tag[1] }
+ }
+
+ fun assemble(check: CheckType) = arrayOf(TAG_NAME, check.code)
+
+ fun assemble(checks: List) = checks.map { assemble(it) }
+ }
+}
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/tags/FrequencyTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/tags/FrequencyTag.kt
new file mode 100644
index 000000000..95c6d7d1a
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/tags/FrequencyTag.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2025 Vitor Pamplona
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.vitorpamplona.quartz.nip66RelayMonitor.monitor.tags
+
+import com.vitorpamplona.quartz.nip01Core.core.has
+import com.vitorpamplona.quartz.utils.ensure
+
+/** ["frequency", ""] - how often this monitor publishes check results */
+class FrequencyTag {
+ companion object {
+ const val TAG_NAME = "frequency"
+
+ fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME
+
+ fun parse(tag: Array): Long? {
+ ensure(tag.has(1)) { return null }
+ ensure(tag[0] == TAG_NAME) { return null }
+ ensure(tag[1].isNotEmpty()) { return null }
+ return tag[1].toLongOrNull()
+ }
+
+ fun assemble(seconds: Long) = arrayOf(TAG_NAME, seconds.toString())
+ }
+}
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/tags/TimeoutTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/tags/TimeoutTag.kt
new file mode 100644
index 000000000..da9a752a3
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/tags/TimeoutTag.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2025 Vitor Pamplona
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.vitorpamplona.quartz.nip66RelayMonitor.monitor.tags
+
+import androidx.compose.runtime.Stable
+import com.vitorpamplona.quartz.nip01Core.core.has
+import com.vitorpamplona.quartz.utils.ensure
+
+/**
+ * Timeout tag for relay monitors.
+ *
+ * General form: ["timeout", ""]
+ * Per-check form: ["timeout", "", ""]
+ *
+ * Where check-type is one of: open, read, write, nip11
+ */
+@Stable
+class TimeoutTag(
+ val checkType: String?,
+ val milliseconds: Long,
+) {
+ companion object {
+ const val TAG_NAME = "timeout"
+
+ fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME
+
+ fun parse(tag: Array): TimeoutTag? {
+ ensure(tag.has(1)) { return null }
+ ensure(tag[0] == TAG_NAME) { return null }
+ return if (tag.has(2)) {
+ val ms = tag[2].toLongOrNull() ?: return null
+ TimeoutTag(checkType = tag[1], milliseconds = ms)
+ } else {
+ val ms = tag[1].toLongOrNull() ?: return null
+ TimeoutTag(checkType = null, milliseconds = ms)
+ }
+ }
+
+ fun assemble(milliseconds: Long) = arrayOf(TAG_NAME, milliseconds.toString())
+
+ fun assemble(
+ checkType: String,
+ milliseconds: Long,
+ ) = arrayOf(TAG_NAME, checkType, milliseconds.toString())
+
+ fun assemble(timeout: TimeoutTag) =
+ if (timeout.checkType != null) {
+ assemble(timeout.checkType, timeout.milliseconds)
+ } else {
+ assemble(timeout.milliseconds)
+ }
+
+ fun assemble(timeouts: List) = timeouts.map { assemble(it) }
+ }
+}
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt
index c570731f9..2870e06fa 100644
--- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt
@@ -121,6 +121,8 @@ import com.vitorpamplona.quartz.nip64Chess.LiveChessGameChallengeEvent
import com.vitorpamplona.quartz.nip64Chess.LiveChessGameEndEvent
import com.vitorpamplona.quartz.nip64Chess.LiveChessMoveEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
+import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.RelayDiscoveryEvent
+import com.vitorpamplona.quartz.nip66RelayMonitor.monitor.RelayMonitorEvent
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent
import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent
@@ -281,6 +283,8 @@ class EventFactory {
ReactionEvent.KIND -> ReactionEvent(id, pubKey, createdAt, tags, content, sig)
ContactCardEvent.KIND -> ContactCardEvent(id, pubKey, createdAt, tags, content, sig)
RelayAuthEvent.KIND -> RelayAuthEvent(id, pubKey, createdAt, tags, content, sig)
+ RelayDiscoveryEvent.KIND -> RelayDiscoveryEvent(id, pubKey, createdAt, tags, content, sig)
+ RelayMonitorEvent.KIND -> RelayMonitorEvent(id, pubKey, createdAt, tags, content, sig)
RelaySetEvent.KIND -> RelaySetEvent(id, pubKey, createdAt, tags, content, sig)
ReportEvent.KIND -> ReportEvent(id, pubKey, createdAt, tags, content, sig)
RepostEvent.KIND -> RepostEvent(id, pubKey, createdAt, tags, content, sig)