What g-pay is, what V1 does, and where V2 takes it.
g-pay is an institutional payments backend on Solana that protects the institution's on-chain identity by combining three classical primitives: per-payment stealth addresses, an AML-gated escrow program, and a no-consolidation treasury pattern. No zero-knowledge proofs.
The problem
On a transparent chain like Solana, an institution's treasury address is a long-lived identity. If a customer pays from a wallet that is later added to a sanctions list, the institution's treasury is now linked, on-chain, to a sanctioned entity. Public ledger means public liability — and unlike private banking, you cannot "not accept" a transaction once it's on chain.
The interesting risk is the address, not the amount. Existing Solana privacy work (Token-2022 confidential transfers, Confidential Balances) hides amounts but leaves addresses public. g-pay is the opposite: amounts stay public, addresses change every payment, and an attestation gate runs beforeany funds touch the institution's side.
Three primitives
The whole stack is the composition of three things any Solana developer already understands. None of them require ZK.
Per-payment stealth addresses (Curve25519)
Cryptonote-style two-key derivation adapted for Curve25519. The institution publishes spend_pub and view_pub. For each payment, the gateway samples r, computes R = r·G, and derives the per-payment stealth pubkey P = spend_pub + H(r·view_pub)·G. Implemented twice — once in the Rust crate stealth-core (used by the indexer) and once in the TypeScript port at apps/api-gateway/src/stealth.ts (used by the gateway). Byte-exact equality is locked by a unit test in both languages.
V2 will combine address-level privacy with amount-level privacy by layering Token-2022 Confidential Balances on top, so the institution can also keep transfer amounts private from the public chain while the AML oracle still sees them.
Quarantine vault (Anchor program)
An Anchor program at programs/quarantine-vault holds incoming SOL or SPL tokens in a per-deposit escrow PDA at state Pending. The Vault account stores an oracle_set (max 16) and a min_attestations threshold. Each oracle in the set can post one Attestation per deposit; once clean_count or dirty_count reaches the threshold, state advances to Approved or Rejected. Released funds leave from the deposit PDA via an explicit release instruction; refunded funds go back to a refund_addr captured at deposit time.
V1 ships the m-of-n state machine, but the oracle set in the demo is just three keypairs that vote however the demo endpoint tells them to. V2 wires real adapters — Chainalysis, TRM Labs, Range — that produce signed attestations from external risk scores. We also add evidence_hash IPFS upload and a per-attestor dispute window.
Hyperscaled treasury ("no consolidation")
Each release in the demo flow goes to a freshly generated pubkey — there is no on-chain link between successive payments to the same institution. This is the Bitcoin UTXO best-practice (one address per receive) reapplied to Solana, where ATA rent (~0.002 SOL) makes per-payment accounts cheap. Note: V1 does not enforce uniqueness in the program; it's a discipline of the demo flow. The institution is free to re-use targets if it wants.
V2 will add a slice planner: an off-chain coin-selection layer that decides which slice receives each release based on an internal balance ledger, plus an aggregate balance UI in the dashboard so the institution can still feel like 'one wallet' while operating across thousands of slices on chain.
End-to-end flow (what actually happens when you click the buttons)
- Initialize a deposit. The dashboard hits
POST /v1/demo/init. The gateway derives a fresh stealth address (P,R,view_tag) using the demo institution's public keys, writes a row to Postgres with statepending, and pre-wiresrefund_addrto the on-server demo wallet. - Customer payment.The dashboard's "Simulate customer payment" button calls
POST /v1/demo/simulate-payment. The gateway shells out to the bundledgpay-clibinary, which submits a real Solana devnet transaction — the program'sdepositinstruction — that creates a newDepositPDA holding 0.1 SOL. - Indexer scan. Roughly every 5 seconds, the indexer (a separate Rust process) calls
getProgramAccountson the program, filtered by Deposit account size. For each candidate it computes the shared secret with the registered slice's view-private key, and if the derived pubkey matches the on-chainstealth_pubkey, it POSTs an internal webhook to the gateway. The gateway updates the deposit row withon_chain_address+ amount. - AML attestation. Two oracles sign
attestinstructions with verdict = clean (or dirty). On the second signature the threshold is reached and the on-chain deposit moves toApproved(orRejected). The next indexer pass mirrors the new state into the gateway record. In V1 the oracles are just keypairs that vote whatever the demo endpoint tells them; the attestation flow is real, the risk-scoring source is not. - Release.Approved deposits can be released. The dashboard's release button generates a fresh target pubkey on the server (a new treasury slice), then submits the program's
releaseinstruction signed by the deposit's recordedrelease_authority. Lamports move from the deposit PDA to the fresh slice. Indexer picks up the new state, gateway recordsreleased. - Refund. Rejected or expired deposits can be refunded to the captured
refund_addr. The program enforces thatrefund_target == deposit.refund_addr.
What V1 ships (and what it doesn't)
Honest table — green is verified working, yellow is real but stubbed, gray is intentionally out-of-scope for V1.
| Component | V1 status | Notes |
|---|---|---|
| Anchor program | Working | 8 instructions on devnet (initialize_vault, deposit/_token, attest, release/_token, refund/_token). LiteSVM tests cover the SOL paths end-to-end. |
| Stealth-address derivation | Working | Rust + TypeScript, byte-exact cross-language test vector locked in unit tests. |
| Indexer (devnet RPC scan) | Working | Polls getProgramAccounts every 5s, view-key match, posts to gateway internal webhook. |
| API gateway (REST + Postgres) | Working | 6 public endpoints + 5 demo endpoints + 1 internal webhook, served behind a Caddy reverse proxy on the server. |
| Dashboard (Next.js on Vercel) | Working | Guided demo flow, deposit detail with stepper, Solana Explorer links per transaction. |
| AML oracle | Stub | The m-of-n attestation flow is real on chain; the verdict source is just keypairs that vote whatever the demo endpoint says. No Chainalysis / TRM yet. |
| Relayer (fee-payer service) | Stub | Service exists with rate limit + admission policy + cosign helper, but the demo flow currently bypasses it (the demo wallet pays its own fees). |
| Hyperscaled treasury aggregator | Stub | The demo flow generates a fresh release target every time, but the program does not enforce uniqueness and there is no slice-balance aggregator UI yet. |
| Webhook delivery to institutions | V2 / out of scope | Internal indexer→gateway webhook works; outbound institution callbacks are V2. |
| SPL deposits in the demo flow | V2 / out of scope | The program supports Token + Token-2022 (deposit_token / release_token / refund_token), but the dashboard only drives the SOL path. CLI subcommand for SPL deposit is also V2. |
| Mainnet deployment | V2 / out of scope | V1 is devnet-only on purpose. Mainnet requires audit + KMS-backed signers + real oracle integrations. |
Repo structure
Each path below corresponds to a real piece of code on GitHub.
| programs/quarantine-vault/ | Anchor program (Rust, SBF). 8 instructions, 2 accounts. |
| crates/stealth-core/ | Curve25519 stealth-address derivation, scan, and spend-key reconstruction. The cross-language test vector lives here. |
| crates/indexer/ | Devnet RPC scanner. Long-running Rust binary that polls the program, view-key matches, posts to the gateway. |
| crates/relayer/ | Axum HTTP fee-payer service. V1 has the policy + cosign helper; not yet wired into the demo path. |
| crates/cli/ | Operator CLI. Used by the gateway demo endpoints to build + sign + submit the actual on-chain transactions. |
| apps/api-gateway/ | Hono + Node + Postgres REST API. 6 public endpoints + 5 demo endpoints + 1 internal webhook. |
| apps/dashboard/ | Next.js 16 + Tailwind frontend. Hosted on Vercel. /, /docs, /api, /deposits/* routes. |
| deploy/ | Docker compose stack (postgres + redis + caddy + 3 services), Postgres migrations, Caddyfile. |
| scripts/ | bootstrap-local.sh, deploy-program-devnet.sh, build-artifacts.sh, deploy-server.sh. |
V2 roadmap
Not promises — directions of work, ordered roughly by impact.
- Real AML oracle. Chainalysis + TRM Labs + Range adapters that turn external risk scores into on-chain attestations, with evidence-hash → IPFS upload and a per-attestor dispute window. The m-of-n primitive stays exactly the same; only the verdict source changes.
- Relayer in the loop.Today the demo flow signs and pays fees from a single demo wallet, so the relayer service is dormant. V2 routes every customer-side and release-side transaction through the relayer's
/v1/submitendpoint so customers don't need SOL for fees and operators get a single billing surface. - Confidential amounts on top. Compose stealth-address (address privacy) with Token-2022 Confidential Balances (amount privacy). The institution gets full transfer privacy from the public chain; auditor-key holders (the institution itself + regulator under court order) still see plaintext.
- Real institution signing. View key in HSM/KMS, no plaintext on disk anywhere. Release authority via remote signer (Ledger / Fireblocks / Squads multisig). API key replaced by signed requests + mTLS.
- Outbound webhooks + retry queue. When a deposit changes state, the gateway POSTs to an institution-supplied URL with HMAC and an idempotency key, retries with exponential backoff, and surfaces failures in the dashboard.
- SPL deposit demo path. The program already supports
deposit_token/release_token/refund_token. V2 adds the matching gpay-cli subcommands and a dashboard token selector so judges can test USDC alongside SOL. - Sub-second detection.Replace the indexer's 5s polling with
programSubscribeover WebSocket. Same match logic, lower latency, fewer RPC calls. - Slice planner.Off-chain coin-selection layer that chooses release targets based on an internal balance ledger, plus an aggregated balance UI so the institution feels like "one wallet" while operating across many slices.
- Domain + TLS. Point a domain at the server, switch Caddy to
your-domain.comfor automatic Let's Encrypt. Until then the dashboard hits the gateway via a Vercel rewrite to bypass mixed-content blocks. - Audit + mainnet. Third-party security review (OtterSec / Sec3 / Neodyme) covering the program + the indexer webhook auth + the relayer admission flow, before any mainnet write.
Caveats
- Demo institution test vectors. The bundled demo uses publicly documented private keys for the institution (
spend_priv = 0102…1f00,view_priv = a0a1…be00). They live incrates/stealth-core/tests/vectors.rson purpose, so the cross-language test can lock byte-exact derivation. Real institutions generate their keys inside an HSM and the gateway only ever sees the public parts. - Demo wallet on disk. The on-server keypair that plays the customer + release authority in the demo flow is funded with devnet SOL only. Its private key sits at
config/demo-wallet.jsonon the server and is not in the public repo (.gitignore covers it). - Oracle keys on disk. Same story for the three oracle keypairs. Real production oracles would each be operated by separate institutions / risk vendors with their own signing infrastructure.
- HTTP only. The server is exposed on its IP over plain HTTP. The dashboard reaches it through a Vercel rewrite (HTTPS to HTTPS, then Vercel proxies HTTP server-side) — that side-steps mixed-content for now but is not a substitute for a domain + TLS.
- See SECURITY.md for the disclosure policy and the full known-gaps list.