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
✓ wallet-setup 3544ms nametag 'p-rpvugo1h9x' minted + binding published (3544ms)
✓ request 5340ms faucet accepted (requestId=9114626, will send 1000000 raw unicity-usd, 5340ms)
✓ receipt 331ms USDU 1000000 raw unit(s) received (transfer.id=c2092dd2c1b3, 331ms)
Status: HEALTHY (3/3 checks passed)
Summary: 6 HEALTHY, 0 DEGRADED, 0 UNREACHABLE (of 6)
# 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.
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 friendly# 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 1The 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.
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.
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.
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.
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.
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.
Drives the full mint-and-verify path rather than poking the HTTP layer with a bogus nametag. The faucet has no probe-only mode and no direct-pubkey shortcut, so verifying real delivery requires running the probe as a one-shot Unicity wallet.
Functional (end-to-end):
- wallet-setup — generates an ephemeral mnemonic, mints a single-use nametag (
p-<random>) on the L3 aggregator, and publishes the kind:30078 binding event on the Nostr relay. Without a registered binding, the faucet's resolver returnsNametag not found— so this step is the gate everything else hangs on. - request —
POST /api/v1/faucet/requestfor 1 raw unit (1e-6) of USDU. Healthy: HTTP 200 +success:true+ arequestId. We capture the faucet's claim about what it will send (amountInSmallestUnits) for the next check. - receipt — subscribes to
transfer:incomingon the wallet and waits up to 10 s for a kind:31113 token-transfer event addressed to our pubkey. The SDK handles NIP-04 decryption + Token deserialization. We assert the delivered token'scoinId == USDUandamount == amountInSmallestUnitsfrom step 2. A mismatch here means the faucet's HTTP response is lying about what its async send actually delivered.
Cost per probe run: one nametag NFT minted on the L3 (unreclaimed), one USDU raw unit (≈ economically zero) from faucet quota, one kind:30078 event left on the relay. End-to-end wall-clock is typically 8–12 s — pace probe runs accordingly. The faucet probe needs both --api-key (or the SDK default) and the network's Nostr relay + aggregator to be reachable; if either is down, this probe reports unreachable even when the faucet itself is fine. See the other services' verdicts in the same report to disambiguate.
This is the only probe that depends on @unicitylabs/sphere-sdk. Rationale and trade-offs documented in CLAUDE.md.
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));- 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.
MIT — see LICENSE.
{ "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 } }