@unicitylabs/infra-probe
v0.4.0
Published
Availability + performance probe for Unicity Network infrastructure (Nostr relay, Aggregator, IPFS gateway, L1 Fulcrum, Market, Faucet). Pre-flight check for e2e tests; CI-friendly JSON output.
Readme
unicity-infra-probe
Tiny availability + performance probe for Unicity Network infrastructure.
Designed as a pre-flight gate for end-to-end test suites and a 5-second smoke test when something feels off. Runs six parallel probes — Nostr relay, L3 Aggregator, IPFS gateway, L1 Fulcrum, the Market intent database, and the test Faucet — exercises both the liveness of each endpoint and the functional write+read+verify path real wallet flows depend on, then reports per-check latency in either a colored human-readable format or single-line JSON.
✅ aggregator https://goggregator-test.unicity.network
✓ health 60ms healthy (db ok, 2 shards ok, 60ms)
✓ json-rpc 7ms OK — structured error: Shard ID not found: 0
✓ submit_commitment 39ms accepted (status=SUCCESS, 39ms)
✓ get_inclusion_proof 7ms proof returned in 7ms
Status: HEALTHY (4/4 checks passed)
✅ nostr wss://nostr-relay.testnet.unicity.network
✓ connect 87ms WebSocket handshake OK
✓ subscribe-kind:1 71ms EOSE in 71ms (5 event(s))
✓ publish-kind:1 190ms published+stored (190ms; read-back 1 event)
✓ publish-kind:4 182ms published+stored (182ms; read-back 1 event)
✓ publish-kind:1059 187ms published+stored (187ms; read-back 1 event)
✓ publish-kind:30078 201ms published+stored (201ms; read-back 1 event)
✓ publish-kind:31113 179ms published+stored (179ms; read-back 1 event)
✓ publish-kind:31115 183ms published+stored (183ms; read-back 1 event)
✓ publish-kind:31116 176ms published+stored (176ms; read-back 1 event)
✓ publish-kind:9 185ms published+stored (185ms; read-back 1 event)
✓ publish-kind:25050 181ms published+stored (181ms; read-back 1 event)
✓ publish-kind:30000 188ms published+stored (188ms; read-back 1 event)
Status: HEALTHY
✅ ipfs https://unicity-ipfs1.dyndns.org
✓ kubo-api 230ms Kubo 0.39.0
✓ gateway-route 3ms HTTP 200 (image/jpeg, 3ms)
✓ ipfs-add 6ms cid=bafkreieu5rue4yh55meuurfzeedvxmpn6riogv4goo6nxg6gn6kxnovv3m
✓ ipfs-fetch 17ms byte-identical roundtrip (256 bytes, 17ms)
Status: HEALTHY (4/4 checks passed)
✅ fulcrum wss://fulcrum.unicity.network:50004
✓ connect 35ms WebSocket handshake OK
✓ server.version 3ms Fulcrum 1.12.0
✓ chain-tip 24ms block 501098
✓ chain-tip-fresh 0ms tip is 0.8min old (target 2min)
✓ tx-index 2ms tx@501097:0 = df056744adac3761…
Status: HEALTHY (5/5 checks passed)
✅ market https://market-api.unicity.network
✓ search 3708ms 20 intent(s) returned (3708ms)
✓ feed-recent 1016ms 10 listing(s) returned (1016ms)
Status: HEALTHY (2/2 checks passed)
✅ faucet https://faucet.unicity.network
✓ request 168ms cleanly rejected probe-nametag (168ms; "Nametag not found: …")
✓ health 13ms HTTP 200 (13ms)
Status: HEALTHY (2/2 checks passed)
Summary: 6 HEALTHY, 0 DEGRADED, 0 UNREACHABLE (of 6)Install
# From npm (once published)
npm install -g @unicitylabs/infra-probe
# From source
git clone https://github.com/unicitynetwork/infra-probe
cd infra-probe
npm install
npm startRequires Node.js ≥ 20.
Usage
unicity-infra-probe # testnet, pretty
unicity-infra-probe --network mainnet # mainnet
unicity-infra-probe --format json # one-line JSON
unicity-infra-probe --format json --pretty-json # indented JSON
unicity-infra-probe --only nostr,aggregator # subset
unicity-infra-probe --timeout 5000 # tighter ceiling
unicity-infra-probe --quiet # only summary line
unicity-infra-probe --no-color # piped-output friendlyPre-flight gate for an e2e suite
# Bash
unicity-infra-probe --quiet || { echo "infra unhealthy — skipping e2e"; exit 1; }
npm run test:e2e
# JSON-aware (parse exit code AND service detail)
report=$(unicity-infra-probe --format json)
nostr_status=$(echo "$report" | jq -r '.services[] | select(.service=="nostr") | .status')
[ "$nostr_status" = "healthy" ] || exit 1CI / scripted use
The CLI exits with a status code derived from the probe outcome:
| Exit | Meaning |
|--:|---|
| 0 | every probed service is healthy |
| 1 | at least one service degraded (slow, partial) |
| 2 | at least one service unreachable (down) |
| 3 | internal CLI error (bad args, etc.) |
The JSON output is shape-stable — service names, check names, status enums (healthy/degraded/unreachable/error for services; pass/fail/warn for checks), and key field names are part of the public API. New optional fields may be added; existing fields will not be renamed without a major version bump.
What each probe checks
Nostr relay
Liveness:
- connect — WebSocket TLS + handshake.
- subscribe-kind:1 —
["REQ", id, {kinds:[1], limit:5}], REQ → EOSE roundtrip. Diagnostic only: afailhere is downgraded towarnand does NOT gate the rest of the probe. The unicity testnet relay's broad-author indexed query path has been observed to degrade independently from the publish + author-indexed read paths that wallets actually use, so this single check is not authoritative for "can wallets use this relay?".
Functional (write+confirm across every Unicity-used kind): 3. publish-kind:N for each kind in the SDK's emit set:
1(BROADCAST) — regular4(DIRECT_MESSAGE / NIP-04) — regular1059(NIP-17 gift wrap) — regular25050(composing indicator / NIP-59) — ephemeral30078(NAMETAG_BINDING / NIP-78 app-data) — parameterized replaceable31113(TOKEN_TRANSFER, Unicity custom) — parameterized replaceable31115(PAYMENT_REQUEST, Unicity custom) — parameterized replaceable31116(PAYMENT_REQUEST_RESPONSE, Unicity custom) — parameterized replaceable
Each kind: signs an ephemeral event with a fresh single-shot keypair, sends ["EVENT", e], waits for ["OK", id, true, ...], and verifies according to the kind's NIP-01 classification:
- regular / replaceable — re-query
{kinds:[N], authors:[ourPubkey]}and confirm the event is stored. - ephemeral (kinds 20000-29999) — only verify the OK ack. Per NIP-01 §16 the relay MUST NOT store ephemeral events, so a read-back-required check would false-fail every healthy relay for kind 25050.
For parameterized replaceable kinds (30000-39999) we always attach a d tag for storage uniqueness.
The relay verdict is determined only by these publish-and-confirm outcomes — connect and subscribe-kind:1 are diagnostics. Each ephemeral keypair is generated per-publish and never persisted; the probe leaves only short-lived events signed by random pubkeys.
Excluded kinds (intentional):
kind 9(NIP-29 group chat) lives on the SDK's separateDEFAULT_GROUP_RELAYS(e.g.wss://chat.unicity.network), not the wallet relay. A futuregroupchatprobe will cover NIP-29.
Aggregator (L3)
Liveness:
- health —
GET /health. Operator-facing endpoint; returns{ status, database, aggregators: {...} }. - json-rpc —
POSTwith a deliberately-invalidshardId. A structuredShard ID not foundreply is healthy (proves the JSON-RPC handler is alive); a non-JSON reply is failure.
Functional (full submit + retrieve roundtrip):
3. submit_commitment — generates an ephemeral secp256k1 keypair, builds a fully-signed SubmitCommitmentRequest (canonical wire format mirrored from @unicitylabs/state-transition-sdk: DataHash imprints, RequestId = SHA-256(pubkey ‖ stateImprint), 65-byte recoverable signature), and submits.
4. get_inclusion_proof — polls for the inclusion proof of the just-submitted commitment for up to 5 s. A returned proof confirms the WRITE was actually persisted into the SMT and is retrievable through the read API.
IPFS gateway
Liveness:
- kubo-api —
POST /api/v0/version; returns Kubo version info. - gateway-route —
HEAD /ipfs/<canonical-cid>; verifies path routing.
Functional (write+read+verify roundtrip):
3. ipfs-add — uploads ~256 bytes of random content via POST /api/v0/add?pin=false&cid-version=1. pin=false keeps the probe stateless: the node will GC the bytes on its next sweep, so we don't need to call pin/rm (which the unicity gateway has locked down anyway).
4. ipfs-fetch — GET /ipfs/<just-added-cid> and asserts byte-identical content match. The byte-comparison is critical because the unicity gateway has been observed to return a placeholder JPEG (HTTP 200 + image/jpeg) for unpinned/missing CIDs — a "HTTP 200 = OK" check would false-pass.
L1 Fulcrum (ALPHA blockchain Electrum server)
Liveness:
- connect — WSS handshake.
- server.version — Electrum-protocol handshake.
- chain-tip —
blockchain.headers.subscribe; current block height + header.
Functional:
4. chain-tip-fresh — decodes the block-header timestamp (offset 68, LE uint32) and asserts it's recent. Healthy: <30 min old. Warn: <2 h. Fail: ≥2 h. ALPHA target block time is 2 min so the typical age is sub-minute.
5. tx-index — blockchain.transaction.id_from_pos [tipHeight − 1, 0]; verifies the node serves indexed historical data, not just the live tip. This is the path wallet history rebuilds and address subscriptions depend on.
Market / Intent database
Liveness:
- search —
POST /api/searchwith{query: "test"}. Healthy: HTTP 200 + JSON body whoseintentsfield is an array (possibly empty). This one call exercises the embedding pipeline (semantic search) end-to-end.
Functional:
2. feed-recent — GET /api/feed/recent. Cross-checks the search engine against the raw feed. If search works but feed/recent doesn't, the embedding pipeline is sick; if both work, the database is fully online.
Embedding
import { runProbes, exitCodeForReport } from '@unicitylabs/infra-probe';
const report = await runProbes({ network: 'testnet', only: ['nostr', 'aggregator'] });
console.log(JSON.stringify(report.summary)); // { total: 2, healthy: 2, degraded: 0, unreachable: 0 }
process.exit(exitCodeForReport(report));Adding a new probe
- Add an
mjsfile undersrc/probes/exporting aprobeXxx(endpoint, opts)function. Return a service-shaped object:{ service, endpoint, status, latencyMs, checks, error?, timestamp }. Thestatusenum is'healthy' | 'degraded' | 'unreachable' | 'error'. Each entry ofchecksfollows{ name, status: 'pass'|'fail'|'warn', latencyMs?, message? }. - Wire the probe into
src/index.mjs(SERVICESarray +thunksmap) and add the endpoint tosrc/networks.mjs. - The pretty + JSON renderers pick up the new service automatically.
Output shape
{
"network": "testnet",
"networkLabel": "Testnet",
"startedAt": "2026-05-01T16:23:45.000Z",
"completedAt": "2026-05-01T16:23:51.000Z",
"services": [
{
"service": "aggregator",
"endpoint": "https://goggregator-test.unicity.network",
"status": "healthy",
"latencyMs": 92,
"checks": [
{ "name": "health", "status": "pass", "latencyMs": 43, "message": "healthy (db ok, 2 shards ok, 43ms)" },
{ "name": "json-rpc", "status": "pass", "latencyMs": 9, "message": "OK — structured error: Shard ID not found: 0" },
{ "name": "submit_commitment", "status": "pass", "latencyMs": 34, "message": "accepted (status=SUCCESS, 34ms)" },
{ "name": "get_inclusion_proof", "status": "pass", "latencyMs": 5, "message": "proof returned in 5ms" }
],
"timestamp": "2026-05-01T16:23:45.832Z"
}
// ...
],
"summary": { "total": 5, "healthy": 5, "degraded": 0, "unreachable": 0 }
}License
MIT — see LICENSE.
