Confidential underwriting pools for Pact Network, built on Umbra Privacy. Submission for the Superteam Earn Umbra Side Track.
Pact is insurance/refund rails for agentic (x402) payments — its capital comes from underwriting pools where LPs deposit USDC, coverage sales pay premiums in, and breaches pay refunds out. Today that's all public on-chain: who underwrites, how much, which pool, premium income, payouts. pact-umbra holds the pool's capital as an Umbra encrypted balance so amounts and cashflows are hidden — on-chain you see a few Umbra deposit/withdraw transactions with encrypted amounts; off-chain the pool keeps a tamper-evident signed share ledger. An observer can't tell who underwrites, how much, which endpoint they back, the loss ratio, or the pool's P&L. The LPs and the operator can. And it runs Pact's actual coverage flow on top of that encrypted balance — the classifier (ok / server_error / latency_breach / client_error / …), computeCoverage pricing, settle_batch semantics (premium-in agent→pool, Treasury fee fan-out, breach refund pool→agent), and optional registration against a real Pact facilitator.
This is a standalone reference prototype — it does not modify Pact's on-chain program. See DESIGN.md for the architecture, exactly which Umbra primitives are used, exactly how the Pact flow is implemented, and an honest list of what's real vs prototype-grade (per-LP share accounting is off-chain here — production puts it on-chain as encrypted state; the settle_batch instruction is replaced by Umbra deposit/withdraw legs; the x402 payment leg is stubbed, as it's pay.sh's job in real Pact).
npm install
npm run smoke # full lifecycle: deposits → premium → breach → withdrawals → views
# or, narrated:
./demo.shMock mode runs entirely in-memory: the ledger math is real; the Umbra/Solana calls are stubbed and printed as [MOCK].
You can also drive it step by step:
npx tsx src/cli.ts init --force # writes pact-umbra.config.json (mock) + pool/treasury/agent/LP keypairs
npx tsx src/cli.ts deposit alice 1000
npx tsx src/cli.ts deposit bob 3000
# an agent buys coverage on paid x402 calls — each runs Pact's classifier + computeCoverage + settle_batch semantics:
npx tsx src/cli.ts cover 0.05 --status 200 --id call-1 # ok → premium accrues to LPs, Treasury takes its bps
npx tsx src/cli.ts cover 0.05 --status 503 --id call-2 # server_error → breach: agent refunded the $0.05 it paid
npx tsx src/cli.ts cover 0.05 --slow --id call-3 # latency_breach → breach
npx tsx src/cli.ts cover 0.05 --status 404 --id call-4 # client_error → uncovered: pool does nothing
npx tsx src/cli.ts public # what an on-chain observer sees (≈ nothing)
npx tsx src/cli.ts view alice # what alice can compute (full P&L)
npx tsx src/cli.ts withdraw alice # exits to a fresh, unlinkable address
npx tsx src/cli.ts state
npx tsx src/cli.ts verify # Ed25519 + hash-chain integrity checkAdd --facilitator https://facilitator.pact.network (or a staging/localhost URL) to init to also POST a real /v1/coverage/register envelope on every cover (best-effort — an outage is ignored, exactly like pact pay).
You need three funded devnet keypairs (SOL for fees + devnet USDC for capital) and an RPC endpoint with WebSocket support:
export PACT_UMBRA_RPC_URL="https://api.devnet.solana.com" # or your provider
export PACT_UMBRA_WS_URL="wss://api.devnet.solana.com"
export PACT_UMBRA_POOL_KEYPAIR=./keys/pool.json # solana-keygen new -o keys/pool.json
export PACT_UMBRA_LP_ALICE_KEYPAIR=./keys/alice.json
export PACT_UMBRA_LP_BOB_KEYPAIR=./keys/bob.json
export PACT_UMBRA_AGENT_KEYPAIR=./keys/agent.json # the covered agent (sources premiums, receives refunds)
# optional:
export PACT_UMBRA_NETWORK=devnet # default
export PACT_UMBRA_USDC_MINT=4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU # devnet USDC (default)
export PACT_UMBRA_INDEXER=https://utxo-indexer.api-devnet.umbraprivacy.com # default for devnet
export PACT_FACILITATOR_URL=https://facilitator.pact.network # optional: best-effort POST /v1/coverage/register per call
npm run smoke # runs against devnet — registers the pool's Umbra user, shields LP capital, runs Pact coverage settlements, unshieldsFund devnet SOL with solana airdrop 2 <pubkey> --url devnet; get devnet USDC from a faucet for that mint, or mint your own and pass it via PACT_UMBRA_USDC_MINT.
The same env vars work for the CLI: npx tsx src/cli.ts init --real --rpc "$PACT_UMBRA_RPC_URL" --ws "$PACT_UMBRA_WS_URL" writes a real-mode config (you'll then point its poolSeed/lps at your funded keypairs, or just use npm run smoke which reads the env vars directly).
| command | what |
|---|---|
npm run smoke |
full lifecycle demo (mock unless real-mode env vars are set) |
./demo.sh |
narrated mock run, screen-record-friendly |
npm test |
ledger math + tamper-detection unit tests |
npm run typecheck |
tsc --noEmit |
npm run build |
compile to dist/ |
npx tsx src/cli.ts help |
CLI usage |
import { UnderwritingPool, parseUsdc } from "pact-umbra"; // (after build; or import from "./src/index.ts" in-repo)
const pool = await UnderwritingPool.create({
network: "devnet",
rpcUrl, rpcSubscriptionsUrl,
poolSecret: "./keys/pool.json", // path | 32-byte seed | 64-byte secret
usdcMint: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
// mock: true, // omit to auto-detect; true = in-memory
});
await pool.deposit({ label: "alice", secret: "./keys/alice.json" }, parseUsdc("1000"));
// Run Pact's coverage flow for a paid x402 call (classify → computeCoverage → settle_batch semantics):
await pool.coverPaidCall({
callId: "call-1",
agentSecret: "./keys/agent.json",
amountPaidBaseUnits: parseUsdc("0.05"), // what the agent paid the upstream (the at-risk amount)
result: { paymentOk: true, httpStatus: 503, latencyMs: 40 }, // → server_error → breach: agent refunded $0.05
latencyBudgetMs: 800,
resource: "https://api.example/data",
});
await pool.withdraw({ label: "alice", secret: "./keys/alice.json" }); // → fresh address
pool.getPublicView(); // what an on-chain observer can see
pool.getLpView("alice"); // full position (only the LP / operator can compute this)
pool.getPoolState();
pool.verifyLedger(); // throws on tamperingsettleCoverage is the lower-level form (you supply the verdict); recordPremium / payBreach are the raw encrypted-balance ops if you want them.
Prototype. Ledger math, tamper-evident signing, and Pact's coverage flow (classifier, computeCoverage, exposure/imputed-cost caps, settle_batch semantics, deterministic coverage id, /v1/coverage/register envelope) are real and unit-tested (19 tests). The Umbra SDK wiring is written against the documented @umbra-privacy/sdk@4.0.0 surface and runs against devnet with funded keypairs, but isn't exercised in CI. Per-LP share accounting is off-chain in this prototype (tamper-evident, but trusts the operator not to withhold/fork) — the production path is on-chain encrypted share state. The settle_batch instruction is replaced by Umbra deposit/withdraw legs; the x402 payment leg is stubbed (it's pay.sh's job in real Pact). Full details in DESIGN.md.
MIT.