This relay is public Internet infrastructure. It assumes every client is hostile until proven otherwise.
- Message size cap before JSON parsing (256 KB default; close 1009).
- Field shape checks before any cryptography: lowercase-hex
id,pubkey,sig; integerkind(0–65535) andcreated_at; tag count; content length. - Event id = sha256(serialization) verified, then Schnorr signature
verified (
@noble/curves, constant-time primitives). created_atfuture limit (default +900 s; NIP-01 SHOULD).- SIP-01 schema validation for kind 39697 (spec §12.4): URL allowlist
(http/https only —
javascript:,data:,file:rejected), d↔u and x↔content hash consistency, hard length caps, extension tag shapes. - Policy engine: pubkey/kind/tag allow+block lists, blocked content phrases, optional NIP-05 gate, optional anti-spam content hashing.
- Rate limits: per-connection token buckets — writes (10/min), reads (50/min), and a roomier kind-39697 indexer bucket (120/min, burst 240).
- Query cost controls: complexity scoring, COUNT precheck against tag
queries (>10k estimated rows refused), global per-REQ event cap, chunked
execution of huge filter arrays, search length/result caps, COUNT
complexity guard, NEG item cap (
NEG-ERR blocked:beyond).
Upstream's payment endpoint marked any caller-supplied ?npub= as paid —
anyone could self-grant access. This fork only records payment from a
cryptographically valid kind 9735 zap receipt:
- valid Schnorr signature;
ptag = the operator's pubkey (decoded from the configured npub);Ptag = the payer receiving access;amount(msats) ≥ configured price and abolt11invoice present;- receipt ≤ 30 days old.
Trust model: a zap receipt is issued and signed by the operator's own
LNURL server after invoice settlement, so trusting it reduces to trusting the
operator's Lightning infrastructure — the correct trust root for a relay
policy decision. A receipt can be replayed, but only to grant access to the
same payer it already paid for. Invoice-level double-checks against the
LNURL verify endpoint can be layered on later without changing the module
interface (outbound calls are deliberately not made by default — see SSRF).
The relay never fetches URLs from events or search queries. The only outbound fetches are:
https://<domain>/.well-known/nostr.jsonfor the optional NIP-05 gate (off by default; domains come from publishing clients' kind-0 metadata — enable only with allow-listed domains if you turn this on), and- the favicon proxy to the configured
relayInfo.iconURL (static config).
Server-side crawling is out of scope for the relay itself — crawlers (Crawlstr/Indexstr) fetch URLs and apply their own SSRF protections; the relay only stores their signed observations.
- Hibernating Durable Objects with idle eviction alarms.
- Max 100 subscriptions per connection; max 20 filters per REQ; sub-id ≤ 64.
- NEG sessions: in-memory, one per sub-id, 10-minute idle reclamation, replaced on repeated NEG-OPEN (per NIP-77).
- DO broadcast mesh validates event ids against a dedupe map before fanout.
- All SQL is parameterized (bound values); LIKE patterns are escaped with
ESCAPE '\'; tag names come from a whitelist for cache-column lookups. - Deletion requires ownership (
etag targets must share the deletion's pubkey;atag targets must match kind+pubkey). - D1 sessions (
withSessionbookmarks) give read-after-write consistency without trusting stale replicas.
- Never in source: Cloudflare API tokens, account IDs, Nostr private keys,
Lightning node keys. Use
wrangler secretfor runtime secrets. - The relay needs no private key at all — it serves and validates; it does not sign.
Optional product surface. Isolation (default): the relay ships with
DEPLOY_SERVICE_ENABLED = false — a public relay should never handle
customer Cloudflare credentials. Run the service as its own Worker:
wrangler deploy --config wrangler.service.toml (entry
src/service-worker.ts, its own D1 SERVICE_DATABASE, no WebSocket/relay
surface at all). The shared modules make the two deploys one codebase, two
security domains.
-
Config validation (P0-2): a zero-address PRE wallet or an undecodable zap npub disables that payment method; if no method is usable, payment and deployment calls fail with 503 naming the problem. Never silently accepts money into a misconfiguration.
-
Bundle pinning (P0-3): customer deployments fetch the CI-built
worker.jsat an immutable commit (DEPLOY_BUNDLE_REF), never a moving branch. SetDEPLOY_BUNDLE_SHA256to also enforce the bundle's hash before provisioning (recommended for production). -
Dual-RPC PRE verification (P1): every Base RPC in
BASE_RPC_URLSis queried and at leastBASE_RPC_QUORUMmust return byte-identical receipts (status + logs) before a PRE payment counts. A single lying RPC cannot fake a payment. -
Deployment idempotency (P1): each paid deploy is a
deploy_jobsrow (provisioning→deployed/failedwith the step log); failures refund the payment credit, and Cloudflare calls are idempotent (D1 create-or-find, worker PUT overwrite, subdomain enable). -
Federation budgets (P1): NEG-OPEN has a per-IP rate (
NEG_OPEN_PER_IP_PER_MIN) and a per-DO concurrent session cap (NEG_MAX_CONCURRENT_SESSIONS) on top of the global item/frame/session limits — negentropy is CPU + D1 work, not just bandwidth. -
Customer Cloudflare tokens arrive over TLS at
POST /api/service/deploy, are used in memory for provisioning calls againstapi.cloudflare.com, and are never logged, stored, cached, or returned. The UI tells customers exactly which scoped permissions to grant (Workers Scripts Edit + D1 Edit) and to delete the token afterwards. The endpoint is rate-limited per IP per day and gated behind a verified payment credit. -
Admin auth is NIP-98: every admin call is a fresh kind 27235 signed event bound to the exact URL, method, and payload hash, with a 5-minute freshness window, owner-pubkey-gated. No sessions or cookies to steal.
-
Lightning payments are verified from kind 9735 zap receipts (same trust model as pay-to-relay) and are single-use per receipt id.
-
PRE payments are verified on-chain via the public Base RPC (
https://mainnet.base.org): confirmed receipt + a Transfer log on the PRE contract (0x3816dd4b…de7a) to the service wallet with value ≥ price. Tx hashes are single-use. Trust note: the RPC response is trusted as Base consensus — an RPC lie could fake a payment; for mainnet-value use, pin a reputable RPC (or two and cross-check) viaBASE_RPC_URL. -
The service deploys a stock-config relay (current
worker.jsfrom the repository) into the customer's account. Customers can reconfigure after deploy with wrangler.
Open a GitHub issue (or contact the operator address in the NIP-11 document). Please include reproduction details; do not include secrets.