DocsGlasshouse
Transparency

Glasshouse — the public audit log

Every state-changing action on Nextbridge is appended to a public, cryptographically-signed, hash-chained log. Anyone can fetch it, anyone can verify it, no one can edit it without breaking the chain.

Public — no authsha256 + HMAC

Overview

Glasshouse is a forward-secure audit log. Each entry contains the hash of the previous entry, so a single retroactive edit anywhere in the chain breaks every entry that followed. Entries are signed with HMAC-SHA256 by Nextbridge so you can also prove an entry actually came from us.

The live feed and history are at /glasshouse. API surface below.

Entry shape

json
1{
2 "id": "cln…",
3 "seq": 142,
4 "ts": "2026-05-22T15:46:23.000Z",
5 "kind": "trade.executed",
6 "actorHash": "a3f2…d8",
7 "data": {
8 "coin": "BTC",
9 "side": "BUY",
10 "executedPrice": "77352.86",
11 "coinAmount": "0.0000554",
12 "usdtAmount": "4.29"
13 },
14 "prevHash": "0000…cf",
15 "entryHash": "7b1c…9e",
16 "signature": "f8d2…42"
17}
  • seq — gap-free auto-increment.
  • actorHash — sha256(userId | deploy-salt); same hash for the same user across the deploy, opaque externally.
  • data — canonical-JSON payload, PII-sanitized.
  • prevHash — entryHash of seq-1, or 64 zeros for the genesis entry.
  • entryHash — sha256 over the canonical preimage {seq, ts, kind, actorHash, data, prevHash}.
  • signature — HMAC-SHA256(GLASSHOUSE_SIGNING_SECRET, entryHash).

Verifying the chain

Two equivalent options. Run the verifier server-side via GET /api/glasshouse/verify, or do it yourself with this ~30 line snippet.

tsverify.ts
1import crypto from 'node:crypto'
2
3function canonicalJson(v: unknown): string {
4 if (v === null) return 'null'
5 if (typeof v === 'string') return JSON.stringify(v)
6 if (typeof v === 'number' || typeof v === 'boolean') return String(v)
7 if (Array.isArray(v)) return '[' + v.map(canonicalJson).join(',') + ']'
8 const obj = v as Record<string, unknown>
9 const keys = Object.keys(obj).sort()
10 return '{' + keys.map(k => JSON.stringify(k) + ':' + canonicalJson(obj[k])).join(',') + '}'
11}
12
13export function verifyEntry(entry: any, predecessorEntryHash: string): boolean {
14 if (entry.prevHash !== predecessorEntryHash) return false
15 const preimage = canonicalJson({
16 seq: entry.seq,
17 ts: entry.ts,
18 kind: entry.kind,
19 actorHash: entry.actorHash,
20 data: entry.data,
21 prevHash: entry.prevHash,
22 })
23 const recomputed = crypto.createHash('sha256').update(preimage).digest('hex')
24 return recomputed === entry.entryHash
25}
bash
1curl "https://nextbridge.exchange/api/glasshouse/verify?from=1&to=100"

Streaming new entries

Open GET /api/glasshouse/stream as a Server-Sent Events connection. Backfill from a known seq via ?since=N. The server emits event: ready once with the current head, then event: entry per new entry. Heartbeats every 30 seconds as comments to keep proxies alive. Connections self-close after 5 minutes; reconnect.

ts
1const es = new EventSource('https://nextbridge.exchange/api/glasshouse/stream')
2es.addEventListener('ready', ev => console.log('head:', ev.data))
3es.addEventListener('entry', ev => {
4 const entry = JSON.parse(ev.data)
5 console.log('new', entry.kind, entry.seq)
6})

Privacy model

The point of Glasshouse is transparency without identity. User identifiers are hashed with a per-deploy salt — the same user has the same actorHash across all their entries (so you can prove your own activity if you want), but an observer can't reverse the hash back to a user.

Sensitive payload fields are stripped at the emit point: on-chain tx hashes (deanonymise deposit addresses), destination withdrawal addresses, raw chat message content, and 2FA codes never enter the log. The schema permits PII; the emitting code does not.

Entry kinds

Kind
Fires when
trade.executed
A spot order fills (executeTrade succeeds).
deposit.credited
An on-chain deposit clears confirmations and is credited to the wallet.
withdrawal.completed
An outbound withdrawal broadcasts successfully.
withdrawal.rejected
A withdrawal failed and the locked funds were refunded.
alert.triggered
A price alert hits its target (one-shot).
mcp.tool.invoked
An MCP tool call completes successfully. Failed calls are NOT logged here (only in the private audit log).
p2p.trade.created
Reserved — emits when P2P webhook fan-out lands.
p2p.trade.paid
Reserved.
p2p.trade.completed
Reserved.
p2p.dispute.opened
Reserved.

P2P lifecycle kinds (p2p.trade.*, p2p.dispute.opened) are reserved in the schema but not yet emitted. They land alongside the matching webhook fan-out.