Audit the receipts
NexusPool signs every record it writes to its own log. The signing key's public half is published, the byte layout that gets signed is published, and the records are served over a plain HTTP endpoint. That is enough for anyone to check our claims with their own code, and this page is the whole procedure.
What a receipt proves, and what it does not. A valid signature proves the record came from the holder of the published key and that not one byte has changed since. It does not prove the pool wrote down everything it should have. Signing makes us accountable for what we publish; it cannot make us accountable for silence. Read the receipts as a commitment we cannot later deny, not as an audit of work you never saw.
1. Get the key
One request, no authentication. The document carries a key per chain, the domain tag, and the field order of the signed bytes.
curl https://nexuspool.io/.well-known/nexuspool-custody.jsonThe keys it serves right now, as x-only 32-byte public keys in hex:
| chain | authority public key |
|---|---|
| btc | 124797e8bc0ec89ed60b77c30e1770c624407f72659dc97aa4c4fd7d9377106c |
| bch | 39e0a44418a2f0d831fc9166ffd4f1373bb6f263d753ed6d08862da8ed4a1d46 |
| ltc | 450a714033c4b01731820bca8c32f00f3eb72c56c14b3c8b43cb0a7d0f357387 |
Pin the key you read here. If it ever changes without us saying so in the changelog, treat every receipt signed under the new one as unverified until you know why.
2. Rebuild the signed bytes
A signature is over a 32-byte digest, not over the record. Concatenate these six fields in this order, then take a single SHA-256 of the result. There is no length prefix on the whole message and no trailing padding.
| field | encoding |
|---|---|
| domain | utf8, no terminator |
| seq | uint64 little-endian |
| type_len | uint8 |
| type | utf8 |
| payload_len | uint16 little-endian |
| payload | utf8, unescaped, printable ASCII only |
The domain is the fixed ASCII string NexusPool/Custody/v1, 20 bytes, with no terminator. It exists so a signature over one kind of record can never be replayed as a valid signature over another.
Two details decide whether your implementation matches ours. The two length fields are little-endian, like seq. And the payload you hash is the unescaped string, the one your JSON parser hands back, never the raw text between the quotes in the response. A payload containing a backslash will verify under one reading and fail under the other.
3. Verify
The signature is 64 bytes, BIP-340 Schnorr over secp256k1, exactly as Taproot uses it. Any library that verifies a Taproot signature verifies these. Pass it the 32-byte digest from step 2 and the x-only public key from step 1.
digest = SHA256(preimage)valid = schnorr_verify(sig64, digest, authority_xonly32)
4. Fetch receipts
Receipts are served by address. The literal address sample returns a pool-wide page so you can test your verifier before pointing it at your own work.
curl 'https://nexuspool.io/api/custody-receipts?addr=sample'
curl 'https://nexuspool.io/api/custody-receipts?addr=<your-payout-address>'Each element has four fields: seq, type, payload and sig. Two types appear. custody.time records a tip arriving and how long the pool took to put fresh work on the wire, and is pool-wide. custody.hash records the work counted for one address over a window.
5. A verifier you can run
Roughly thirty lines, two dependencies, no NexusPool code. It reads the key from the well-known document rather than trusting a constant, which is the point: nothing here asks you to take our word for a value.
import { sha256 } from '@noble/hashes/sha2.js'
import { schnorr } from '@noble/curves/secp256k1.js'
const doc = await (await fetch('https://nexuspool.io/.well-known/nexuspool-custody.json')).json()
const key = doc.authorities.find(a => a.chain === 'btc').pubkey
const enc = new TextEncoder()
const hex = h => Uint8Array.from(h.match(/../g).map(x => parseInt(x, 16)))
function msg32(type, payload, seq) {
const d = enc.encode(doc.domain), t = enc.encode(type), p = enc.encode(payload)
const m = new Uint8Array(d.length + 8 + 1 + t.length + 2 + p.length)
let o = 0
m.set(d, o); o += d.length
let s = BigInt(seq)
for (let i = 0; i < 8; i++) { m[o++] = Number(s & 0xffn); s >>= 8n }
m[o++] = t.length; m.set(t, o); o += t.length
m[o++] = p.length & 0xff; m[o++] = (p.length >> 8) & 0xff
m.set(p, o)
return sha256(m)
}
const { receipts } = await (await fetch('https://nexuspool.io/api/custody-receipts?addr=sample')).json()
for (const r of receipts) {
const ok = schnorr.verify(hex(r.sig), msg32(r.type, r.payload, r.seq), hex(key))
if (!ok) throw new Error('receipt ' + r.seq + ' does not verify')
}
console.log(receipts.length + ' receipts verified')If a receipt fails, change one character of its payload and run it again. A verifier that still says valid is not checking what it thinks it is checking.
Where this fits
The browser version of this lives on Glass Ledger, which does the same arithmetic in the page and shows the preimage byte by byte. The rest of the machinery is on Technology.