Hindsight

SDK

The app is built on two packages, and your own integration can use them the same way:

  • @hindsight/contract: the typed client for the service's API and the protocol's constants.
  • @hindsight/sdk: commitments, X-Wing envelopes, range proofs, v1 transaction builders, wallet signing, balance records and payroll. It runs in the browser and in Node.

Every secret the SDK handles (keys, openings, amounts, memos) stays in the process that calls it. The service receives only signed transactions, opaque envelopes and public references.

Install

npm install https://hindsight.cash/sdk/hindsight-contract.tgz https://hindsight.cash/sdk/hindsight-sdk.tgz

The range-proof engine ships inside the SDK as a pinned WebAssembly file, @hindsight/sdk/proofs.wasm. In the browser, serve that file from your own origin and point the SDK at it:

import { initializeRangeProofs } from '@hindsight/sdk'

await initializeRangeProofs({ wasmUrl: '/hindsight-proofs.wasm' })

In Node the SDK reads the file from the package itself. A file whose digest doesn't match the pinned one is refused.

Connect to the service

import { createApiClient } from '@hindsight/contract'

const api = createApiClient({ apiOrigin: 'https://api-production-9003.up.railway.app' })
const runtime = await api.call('runtimeConfig', { params: {}, query: {}, body: {} })
// runtime.value.data: the cluster, the program ID, the RPC URL to use and, for each pool,
// the pool with its policy and vault, its mint, and the auditor's current key record

Read the program ID, the pool and the auditor's key from the runtime, never from a constant in your code: they are what the service is running against.

Keys

Each participant holds two keys of their own, created where the SDK runs and never derived from a wallet signature:

import { createCryptoClient, generateSelfRecordKey } from '@hindsight/sdk'

const envelopes = createCryptoClient()
const xwing = await envelopes.generateKeyPair()     // the X-Wing key notes are sealed to
const selfRecord = await generateSelfRecordKey() // encrypts your balance record on chain

The X-Wing public key is 1,216 bytes and its private key is a 32-byte seed. Register the public key on chain with register_key; keep both keys where you keep other secrets.

Sign in

Mutating API calls and your inbox need a session. The wallet signs a one-use challenge:

import { bytesToHex, createWalletSigner } from '@hindsight/sdk'

const signer = createWalletSigner({ address, signMessage, signTransaction })
const challenge = await api.call('authChallenge', {
  params: {}, query: {},
  body: { wallet: signer.address, origin: window.location.origin },
})
const signatureHex = bytesToHex(await signer.signMessage(challenge.value.data.message))
await api.call('authVerify', {
  params: {}, query: {},
  body: { wallet: signer.address, challengeId: challenge.value.data.challengeId, signatureHex },
})

signMessage and signTransaction come from the wallet: the Wallet Standard features solana:signMessage and solana:signTransaction, with transaction version 1 among its supported versions. The signer checks every signature the wallet returns, and that the transaction it signed is the one it was given.

Pay

import { commit, openingAfterTransfer, generateRangeProof, buildTransaction } from '@hindsight/sdk'

// 1. commit to the amount with a fresh blinding
const note = commit(250_000_000n, { memo: 'Invoice 2026-118' }) // 250 USDC in base units

// 2. seal one envelope to the payee and one to the auditor
const pair = await envelopes.sealToRecipients({
  opening: note.opening,
  payee: { publicKey: payeeKey, context: { ...context, role: 'payee', keyEpoch: payeeEpoch } },
  auditor: { publicKey: auditorKey, context: { ...context, role: 'auditor', keyEpoch: auditorEpoch } },
})

// 3. hand the envelopes to the relay
await api.call('relayEnvelopes', {
  params: { hash: pairHashHex }, query: {},
  body: { note: noteAddress, envelopeHashHex: pairHashHex, payeeEnvelopeHex, auditorEnvelopeHex },
}, { idempotencyKey: crypto.randomUUID() })

// 4. prove the note and your remaining balance are in range, then build and sign the transfer
const remaining = openingAfterTransfer(balanceOpening, note.opening)
const rangeProof = await generateRangeProof({
  instruction: 'VerifyBatchedRangeProofU128',
  commitments: [note.commitment, commit(remaining.amount, { blinding: remaining.blinding }).commitment],
  openings: [note.opening, remaining],
  bitLengths: [64, 64],
})
const built = await buildTransaction({ request: { instruction: 'transfer', /* … */ rangeProof }, /* … */ })
const signed = await signer.signTransaction(built)
// send signed.wireBytes through your RPC, then confirm its signature

The envelope context binds the pool, the sender, the payee's wallet, the sender's sequence, the key epoch, the commitment and the role. Both envelopes name the payee's wallet as the recipient; the transfer instruction names the payee's account. Keep the note's opening: if the note comes back, you accept it with that opening.

Deposit and withdraw

Both pay the pool's ramp fee to the protocol fee account. Read the rate from the runtime's policy.rampFeeBasisPoints; the builders derive the fee account themselves.

import { rampFee, openingAfterDeposit, openingAfterWithdraw } from '@hindsight/sdk'

const fee = rampFee(amount, policy.rampFeeBasisPoints) // rounded down
const afterDeposit = openingAfterDeposit(balanceOpening, amount - fee) // the balance gains a − f
const afterWithdraw = openingAfterWithdraw(balanceOpening, amount) // the balance loses a; your wallet gets a − f

The instruction amount is always the gross amount a.

Receive

import { envelopePairHash, checkOpening, openingAfterAccept } from '@hindsight/sdk'

const { envelopes: pair } = (await api.call('envelopes', { params: { hash }, query: {}, body: {} })).value.data
// check the pair against the hash the note carries on chain, then open your envelope
const opened = await envelopes.open({ envelope: payeeEnvelope, recipientKeyPair: xwing, context })
if (!checkOpening(noteCommitment, opened)) throw new Error('Return this note.')

open decrypts and checks the opening against the commitment before it returns. Accept only notes that opened and checked, and, in a pool that requires it, that the auditor has attested. After an accept, your new balance opening is openingAfterAccept(current, [opened, …]).

Balance records

Your account on chain keeps a 68-byte record of your balance's opening, encrypted with your self-record key and bound to the pool, the account, its sequence and its commitment. Every transaction that changes your balance writes a new one. The SDK seals and opens it:

import { sealSelfRecord, openSelfRecord } from '@hindsight/sdk'

So a device with your keys can recover your balance's opening from the chain alone.

Payroll

parsePayrollCsv, planPayroll, preparePayroll and executePayroll run a batch in the payer's browser or process: parsing, the plan checked against the chain, ordered transfer chains with their proofs, and an encrypted journal that a run resumes from. The payroll page describes the file and the run.

Sizes

PartBytes
X-Wing public key1,216
X-Wing ciphertext in an envelope1,120
One envelope1,208
Note plaintext (amount, blinding, memo)72
Memo32
Balance record68
Commitment32