Sirius Labs/ docs

Core Protocol

Solana made execution fast. The edges around execution (offline authorization, signing isolation, and receiver privacy) remain exposed. Sirius Labs secures them with three composable layers, each closing a gap the previous one leaves open.

1

MORA

Pay on Solana with zero internet at transaction time.

Lock funds once into an escrow. Sign compact offline vouchers (84-byte message + 64-byte signature). Settle on-chain when reconnected.

2

AirSign

The signing key never touches a networked device.

MORA vouchers are signed on a permanently air-gapped device. The signature travels back via fountain-coded QR codes. No USB, no Bluetooth, no attack surface.

3

GrayBox

Accept payments without revealing wallet identity.

Each payment is routed to a one-time x25519 ECDH stealth address derived per transfer. The recipient's identity stays off-chain entirely.

+

Optional · ZK Shielded Pool

Break the deposit-withdrawal link entirely.

The stack is composable with Groth16 ZK shielded pools (e.g. Cloak) for full unlinkability. Independent 3rd-party integration, not required to use MORA, AirSign, or GrayBox.

Each layer is independently deployable. Use MORA alone, add AirSign for signing isolation, add GrayBox for receiver privacy, or run the full stack.

Architecture

The three layers are independently deployable but designed to compose. Each closes a distinct attack surface. In a full-stack payment they are applied in this order: GrayBox first (recipient setup), then AirSign (offline signing), then MORA (on-chain settlement).

Layer Stack

OrderLayerClosesOutput
1GrayBoxDeanonymization: recipient identity never appears on-chainOne-time x25519 ECDH stealth address
2AirSignKey exposure: signing key never touches a networked deviceEd25519 signature via fountain QR
3MORAConnectivity: payment authorized without live RPC84-byte signed voucher, Receipt PDA

Composition Flow

A full-stack payment follows this path:

  1. Bob publishes a GrayBox Meta-Address (spendPub + viewPub).
  2. Alice derives a one-time stealth vault address from Bob's Meta-Address.
  3. Alice builds an 84-byte MORA message targeting the stealth vault and signs it air-gapped via AirSign, producing a 148-byte signed voucher.
  4. The signed voucher travels back to Alice's online device via fountain-coded QR. No USB, no Bluetooth.
  5. Bob's device submits the signed voucher on-chain. Solana's native Ed25519Program verifies the signature atomically. A Receipt PDA is created. Replay is impossible.
  6. Bob's GrayBox indexer detects the deposit using his view key. Funds enter the AML-gated quarantine vault.
  7. After AML attestation, Bob calls release to move funds to treasury. No on-chain link to sender or stealth address.
Optional ZK layer: The voucher can target a ZK shielded pool (e.g. Cloak) instead of a stealth vault directly.
This adds Groth16 proof-based unlinkability between deposit and withdrawal, breaking the link entirely.
Cloak is an independent 3rd-party system, not required.

On-chain Programs

ProgramNetworkAddress
MORA escrowDevnet9fcXHD3pHDKLX79JuVgCEKQiqYkvVqFtpoAEVjBq4aJ8
GrayBox quarantine vaultDevnet75HuPfb2n7SD7KtcQnVpCW5SVN3RP9gZ9vTXP4D4ha6C

Wallet Connection Setup

All on-chain interactions require a connected Solana wallet. Use the official @solana/wallet-adapter packages to support Phantom, Backpack, and other wallets.

Installation

bash
npm install @solana/wallet-adapter-react \
           @solana/wallet-adapter-react-ui \
           @solana/wallet-adapter-wallets \
           @solana/wallet-adapter-base \
           @solana/web3.js

Provider Setup (Next.js App Router)

tsx
// components/WalletProvider.tsx
"use client";

import { FC, ReactNode, useMemo } from "react";
import {
  ConnectionProvider,
  WalletProvider,
} from "@solana/wallet-adapter-react";
import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";
import { PhantomWalletAdapter } from "@solana/wallet-adapter-wallets";
import "@solana/wallet-adapter-react-ui/styles.css";

const DEVNET_RPC = "https://api.devnet.solana.com";

export const AppWalletProvider: FC<{ children: ReactNode }> = ({ children }) => {
  const wallets = useMemo(() => [new PhantomWalletAdapter()], []);

  return (
    <ConnectionProvider endpoint={DEVNET_RPC}>
      <WalletProvider wallets={wallets} autoConnect>
        <WalletModalProvider>{children}</WalletModalProvider>
      </WalletProvider>
    </ConnectionProvider>
  );
};

Building an AnchorProvider

tsx
"use client"; // this hook can only be used in client components

import { useMemo } from "react";
import { AnchorProvider, Program } from "@coral-xyz/anchor";
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
import { PublicKey } from "@solana/web3.js";
import idl from "./mora_idl.json"; // download from GitHub repo

const MORA_PROGRAM_ID = new PublicKey(
  "9fcXHD3pHDKLX79JuVgCEKQiqYkvVqFtpoAEVjBq4aJ8"
);

function useMoraProgram() {
  const { connection } = useConnection();
  const wallet = useWallet();

  return useMemo(() => {
    const provider = new AnchorProvider(
      connection,
      wallet as any, // wallet-adapter satisfies AnchorWallet interface
      { commitment: "confirmed" }
    );
    return new Program(idl, MORA_PROGRAM_ID, provider);
  }, [connection, wallet]);
}

MORA: Offline Payment Escrow

DevnetAnchor 0.32Ed25519

MORA lets users pay on Solana with zero internet at transaction time. Alice locks funds once into an escrow PDA. This is the only moment internet is required. From that point, she signs MORA vouchers (84-byte message, 64-byte detached signature) completely offline. Bob settles whenever he reconnects.

On-chain Program

Program ID (devnet): 9fcXHD3pHDKLX79JuVgCEKQiqYkvVqFtpoAEVjBq4aJ8

Instructions

InstructionDescription
create_escrowLock SOL into a PDA derived from [payer, seed]. Internet required only once.
settleSubmit 2-instruction tx: Ed25519 native verify + settle ix. Transfers lamports, creates Receipt PDA.
close_escrowAfter expiry (configurable at creation, default 30 days on devnet), reclaims rent + remainder.

Voucher Format

The signed message is 84 bytes: "MORA" (4) | escrow (32) | nonce_u64_le (8) | payee (32) | amount_u64_le (8)

The complete voucher transmitted to Bob is 148 bytes: 84-byte message + 64-byte detached Ed25519 signature.
Both fit comfortably in a single QR code (QR v6 supports up to 224 bytes of binary data).

Security Guarantees

MORA enforces three independent guarantees on-chain, none of which require the payer to be online at transaction time.

AttackWhat an attacker triesHow MORA stops it
Double-spendGive the same signed voucher to two different payeesEach nonce deterministically derives a unique Receipt PDA. The settle instruction initializes it with an init constraint, meaning it can only be created once. A second settle with the same nonce fails with already in use.
Double-settlementSubmit the same voucher on-chain twice (e.g. from two devices)Same mechanism: the Receipt PDA already exists after the first settlement. The tx is rejected before any lamports move.
Signature forgeryForge or tamper with a voucher without the payer's private keyEach settle call is a 2-instruction atomic tx: Solana's native Ed25519 program verifies the signature first. If it fails, the entire tx is rejected. The escrow is never touched.
Why this matters offline: The payer signs vouchers without internet. There is no server to check for replays. MORA moves replay protection entirely on-chain. The Receipt PDA is the canonical record, and it is immutable once created.

Installation

bash
npm install @coral-xyz/anchor @solana/web3.js tweetnacl
# Download IDL:
# https://github.com/nzengi/mora/blob/main/target/idl/mora.json

TypeScript Types

ts
import { PublicKey } from "@solana/web3.js";
import { BN } from "@coral-xyz/anchor";

interface MoraVoucher {
  escrow: PublicKey;
  nonce: bigint;         // u64 — increment per payment
  payee: PublicKey;
  amount: bigint;        // lamports (u64)
  signature: Uint8Array; // 64-byte Ed25519 sig
}

interface EscrowAccount {
  payer: PublicKey;
  amount: BN;
  seed: BN;
  expiresAt: BN;
}

Escrow PDA Derivation

ts
import { PublicKey } from "@solana/web3.js";
import { BN } from "@coral-xyz/anchor";
import { Buffer } from "buffer";

const MORA_PROGRAM_ID = new PublicKey(
  "9fcXHD3pHDKLX79JuVgCEKQiqYkvVqFtpoAEVjBq4aJ8"
);

function deriveEscrowPda(payer: PublicKey, seed: number): PublicKey {
  const seedBuffer = Buffer.alloc(8);
  seedBuffer.writeBigUInt64LE(BigInt(seed)); // Date.now() fits in u64 safely

  const [escrowPda] = PublicKey.findProgramAddressSync(
    [payer.toBuffer(), seedBuffer],
    MORA_PROGRAM_ID
  );
  return escrowPda;
}

// Usage — wallet.publicKey comes from useWallet() (see Wallet Setup section)
const escrowPda = deriveEscrowPda(wallet.publicKey, Date.now());

Building nonceBuffer and amountBuffer

Browser note: Buffer is not available in the browser by default. In client components, add import { Buffer } from "buffer"; and install with npm install buffer.
ts
import { Buffer } from "buffer"; // npm install buffer (browser polyfill)

// Both nonce and amount are 8-byte little-endian unsigned 64-bit integers

// nonce — increment per voucher to prevent replay
const nonce = BigInt(1); // start at 1, increment each payment
const nonceBuffer = Buffer.alloc(8);
nonceBuffer.writeBigUInt64LE(nonce);

// amount — payment in lamports (1 SOL = 1_000_000_000 lamports)
const lamports = BigInt(500_000_000); // 0.5 SOL
const amountBuffer = Buffer.alloc(8);
amountBuffer.writeBigUInt64LE(lamports);

TypeScript Integration

ts
import { AnchorProvider, Program, BN } from "@coral-xyz/anchor";
import { PublicKey, Keypair } from "@solana/web3.js";
import { Buffer } from "buffer"; // npm install buffer
import nacl from "tweetnacl";
import idl from "./mora_idl.json";
// MoraVoucher and EscrowAccount interfaces: see TypeScript Types section above

const MORA_PROGRAM_ID = new PublicKey(
  "9fcXHD3pHDKLX79JuVgCEKQiqYkvVqFtpoAEVjBq4aJ8"
);

async function createEscrow(
  program: Program,
  wallet: { publicKey: PublicKey },
  totalLamports: bigint
): Promise<PublicKey> {
  const seed = Date.now(); // number — fits in u64 safely
  const seedBuffer = Buffer.alloc(8);
  seedBuffer.writeBigUInt64LE(BigInt(seed));

  const [escrowPda] = PublicKey.findProgramAddressSync(
    [wallet.publicKey.toBuffer(), seedBuffer],
    MORA_PROGRAM_ID
  );

  await program.methods
    .createEscrow(new BN(totalLamports.toString()))
    .accounts({ payer: wallet.publicKey, escrow: escrowPda })
    .rpc();

  return escrowPda;
}

function signVoucher(
  escrow: PublicKey,
  nonce: bigint,
  payee: PublicKey,
  amount: bigint,
  keypair: Keypair
): MoraVoucher {
  const nonceBuffer = Buffer.alloc(8);
  nonceBuffer.writeBigUInt64LE(nonce);

  const amountBuffer = Buffer.alloc(8);
  amountBuffer.writeBigUInt64LE(amount);

  const msg = Buffer.concat([
    Buffer.from("MORA"),
    escrow.toBuffer(),
    nonceBuffer,
    payee.toBuffer(),
    amountBuffer,
  ]);

  const signature = nacl.sign.detached(msg, keypair.secretKey);
  return { escrow, nonce, payee, amount, signature };
}

async function settleVoucher(
  program: Program,
  voucher: MoraVoucher,
  settler: { publicKey: PublicKey }
): Promise<string> {
  try {
    const txSig = await program.methods
      .settle(Buffer.from(voucher.signature))
      .accounts({ settler: settler.publicKey, escrow: voucher.escrow })
      .rpc({ commitment: "confirmed" });
    return txSig;
  } catch (err: unknown) {
    if (err instanceof Error && err.message.includes("already in use")) {
      throw new Error(`Nonce ${voucher.nonce} already settled. Use a fresh nonce.`);
    }
    throw err;
  }
}

Error Handling

ts
try {
  await settleVoucher(program, voucher, settler);
} catch (err: unknown) {
  const msg = err instanceof Error ? err.message : String(err);

  if (msg.includes("already in use")) {
    console.error("Voucher already redeemed. Nonce:", voucher.nonce);
  } else if (msg.includes("InvalidSignature")) {
    console.error("Voucher signature invalid");
  } else if (msg.includes("EscrowExpired")) {
    console.error("Escrow expired. Call close_escrow to recover funds.");
  } else if (msg.includes("InsufficientFunds")) {
    console.error("Escrow underfunded for this voucher amount");
  } else {
    throw err;
  }
}

Tests

bash
# Localnet
solana-test-validator --reset --quiet &
anchor test --skip-local-validator

# Devnet smoke
anchor test --skip-local-validator --skip-deploy --provider.cluster devnet

View MORA on GitHub →

AirSign: Air-Gapped Signing

Ed25519Fountain QRArgon2id

MORA already removes internet from the payment moment, but the signing key still lives on a device that touches networks. AirSign closes that gap: the signing key never touches a networked device. MORA vouchers are signed on a permanently air-gapped device; the signature travels back via fountain-coded QR codes with no USB, no Bluetooth, no attack surface.

How It Works

text
Online device          Offline device
──────────            ──────────────
TX → QR frames   →   Scan + reconstruct
                      ↓
Scan signature   ←   Sign → QR frames
↓
Submit to Solana

Key Derivation

Keys are derived using Argon2id KDF from a passphrase. No seed phrase export, no USB, no Bluetooth. The QR channel uses fountain codes that tolerate significant frame loss, approximately 30% at practical QR frame counts (RaptorQ / RFC 6330: ~1-2% encoding overhead, allowing recovery from proportionally high erasure rates).

Setup

DeviceRoleURL
Offline (signer)Scans TX QR, signs, returns signature QRair-sign-signer-web.vercel.app/?role=signer
Online (coordinator)Displays TX as QR, scans signature, submitsair-sign-signer-web.vercel.app/?role=coordinator
Put the offline device in airplane mode before opening the signer URL. Once loaded, it never needs internet again. The key and WASM runtime are fully local.

AirSign vs Hardware Wallet

CapabilityHardware WalletAirSign
No USB / Bluetooth attack surface
M-of-N multisigvendor-dependent
Open, auditable stackpartial
Fountain code frame loss tolerancen/a
Native browser WASMadapter only

Try AirSign Portal →

GrayBox: Receiver Privacy

Devnetx25519 ECDHNo ZKPre-audit

MORA hides when you pay. AirSign hides how you sign. GrayBox hides who receives. x25519 ECDH stealth address derivation (using Ed25519 keys via Curve25519) ensures the recipient's real wallet never appears on-chain. Every payment lands at a unique one-time address with no traceable link back to the recipient's identity. Privacy is achieved without any zero-knowledge dependency.

AML-gated quarantine vault: Funds deposited to a stealth address enter an on-chain quarantine vault. An AML oracle, a permissioned off-chain service that submits an on-chain attestation transaction, verifies funds are clean before the release instruction can be called. In the current devnet implementation, Sirius Labs operates the oracle. The design supports pluggable oracle providers: any party with the oracle keypair can submit attestations, enabling institutional or third-party AML providers. This gives institutions address-level privacy with full auditability. There is no on-chain link between sender, receiver, and treasury, while a compliant audit trail is preserved.

On-chain Program

Program ID (devnet): 75HuPfb2n7SD7KtcQnVpCW5SVN3RP9gZ9vTXP4D4ha6C

How It Works

The Indexer is a GrayBox server-side component that continuously polls Solana program accounts using Bob's view key. When it detects a matching deposit, it notifies the gateway which triggers the AML attestation flow.

StepActorAction
1BobGenerates Meta-Address (spendPub + viewPub)
2AliceDerives one-time stealth address from Meta-Address via API
3AliceDeposits funds into quarantine vault at stealth address
4IndexerScans chain with Bob's view key, detects deposit, notifies gateway
5AML OracleAttests funds are clean. Quarantine gate opens.
6BobReleases funds to treasury (no on-chain link to sender or stealth address)

Steps 3, 5, and 6 are on-chain Anchor program calls (deposit, attest, release). Step 4 (Indexer) is an off-chain service. See the quarantine-vault program on GitHub for the IDL and instruction accounts.

TypeScript Types

ts
interface GrayBoxMetaAddress {
  spendPub: string; // base58 Ed25519 public key
  viewPub: string;  // base58 Ed25519 public key
}

interface StealthAddressResponse {
  stealthPubkey: string;   // base58 — send funds here
  ephemeralPub: string;    // needed for recipient to scan
  viewTag: number;         // single-byte scan optimization
}

interface ViewingKeyResponse {
  viewingKey: string;      // used for compliance scanning
}

API Key

bash
# Demo API key (devnet only)
x-api-key: g-p_demo_h6kj9d8s7g6f5d4

# Production keys: hello@siriuslabs.tech

API Endpoints

http
Base URL: https://graybox-cloak-production.up.railway.app

POST /v1/receiving-address
  Body: { spendPub: string, viewPub: string }
  Returns: StealthAddressResponse

POST /v1/compliance/viewing-key
  Headers: x-api-key
  Returns: ViewingKeyResponse

TypeScript SDK

ts
const GRAYBOX_URL = "https://graybox-cloak-production.up.railway.app";

// Derive a one-time stealth address for incoming payment
async function getStealthAddress(
  meta: GrayBoxMetaAddress,
  apiKey: string
): Promise<StealthAddressResponse> {
  const res = await fetch(`${GRAYBOX_URL}/v1/receiving-address`, {
    method: "POST",
    headers: { "x-api-key": apiKey, "Content-Type": "application/json" },
    body: JSON.stringify(meta),
  });
  if (!res.ok) {
    const text = await res.text();
    throw new Error(`GrayBox API error ${res.status}: ${text}`);
  }
  return res.json() as StealthAddressResponse;
}

// Generate a compliance viewing key (institutions / auditors)
async function getViewingKey(apiKey: string): Promise<ViewingKeyResponse> {
  const res = await fetch(`${GRAYBOX_URL}/v1/compliance/viewing-key`, {
    method: "POST",
    headers: { "x-api-key": apiKey },
  });
  if (!res.ok) {
    const text = await res.text();
    throw new Error(`GrayBox API error ${res.status}: ${text}`);
  }
  return res.json() as ViewingKeyResponse;
}

// Usage — stealth address
try {
  const { stealthPubkey } = await getStealthAddress(
    { spendPub: recipient.spendPub, viewPub: recipient.viewPub },
    "g-p_demo_h6kj9d8s7g6f5d4"
  );
  // stealthPubkey is now safe to use as the payment destination
} catch (err) {
  console.error("Failed to derive stealth address:", err);
}

Live Dashboard →

ZK Shielded Pool: Optional Integration

Groth16ZK Proofs3rd-party

The three core layers leave one gap: a determined observer can still see that a deposit happened, and later that a withdrawal happened. A ZK shielded pool breaks even that link. The Sirius stack is designed to route through any Groth16-based pool. Cloak is the reference integration.

Cloak is an independent ZK shielded pool. Sirius Labs does not operate Cloak. We design our products to be composable with it and similar systems. MORA, AirSign, and GrayBox work fully without a ZK pool. For integration inquiries: hello@siriuslabs.tech

How the Stack Connects

StepActorWhat happens
1GrayBoxDerives one-time stealth address for recipient
2SenderDeposits into ZK shielded pool targeting stealth address
3PoolGenerates Groth16 proof (inputs = outputs, link hidden)
4RecipientWithdraws. On-chain record is only a proof, not a link.

Live Proof of Concept (Mainnet)

StepTX (short)Network
Deposit into shielded pool3bpjSixv...Mainnet →
ZK withdrawal4mLPo2J8...Mainnet →

Integration Interface

ts
// Generic interface — implement for any ZK shielded pool
interface ZkPoolAdapter {
  deposit(params: {
    amount: number;       // lamports
    recipient: string;    // stealth pubkey from GrayBox
  }): Promise<{ txSignature: string }>;

  withdraw(params: {
    amount: number;
    nullifier: string;    // spend nullifier
    proof: Uint8Array;    // Groth16 proof
  }): Promise<{ txSignature: string }>;
}

View Proof of Concept →

Quick Start: Full Stack Integration

End-to-end flow: MORA escrow (online, once) → GrayBox stealth address → AirSign air-gapped signing → MORA settle (+ optional ZK pool deposit).

1. Fund escrow: MORA (online, once)

tsx
// components/SetupEscrow.tsx
"use client";

import { useWallet, useConnection } from "@solana/wallet-adapter-react";
import { AnchorProvider, Program, BN } from "@coral-xyz/anchor";
import { PublicKey } from "@solana/web3.js";
import { Buffer } from "buffer"; // npm install buffer
import idl from "./mora_idl.json";

const MORA_PROGRAM_ID = new PublicKey(
  "9fcXHD3pHDKLX79JuVgCEKQiqYkvVqFtpoAEVjBq4aJ8"
);

export function SetupEscrow() {
  const { connection } = useConnection();
  const wallet = useWallet();

  async function handleSetup() {
    if (!wallet.publicKey) return;

    const provider = new AnchorProvider(connection, wallet as any, {
      commitment: "confirmed",
    });
    const program = new Program(idl, MORA_PROGRAM_ID, provider);

    const seed = Date.now();
    const seedBuffer = Buffer.alloc(8);
    seedBuffer.writeBigUInt64LE(BigInt(seed));

    const [escrowPda] = PublicKey.findProgramAddressSync(
      [wallet.publicKey.toBuffer(), seedBuffer],
      MORA_PROGRAM_ID
    );

    await program.methods
      .createEscrow(new BN("2000000000")) // 2 SOL budget
      .accounts({ payer: wallet.publicKey, escrow: escrowPda })
      .rpc({ commitment: "confirmed" });

    console.log("Escrow funded:", escrowPda.toBase58());
    // Save escrowPda.toBase58() — needed for every voucher
  }

  return (
    <button
      onClick={handleSetup}
      style={{ padding: "8px 20px", borderRadius: "8px", background: "#1d4ed8", color: "#fff", fontWeight: 600, cursor: "pointer" }}
    >
      Fund Escrow
    </button>
  );
}

2. Get stealth address: GrayBox

ts
const API_KEY = "g-p_demo_h6kj9d8s7g6f5d4"; // devnet demo key

const { stealthPubkey } = await getStealthAddress(
  { spendPub: recipient.spendPub, viewPub: recipient.viewPub },
  API_KEY
);
// stealthPubkey — one-time address, safe to use as payment destination

3. Sign voucher offline: MORA + AirSign

ts
import nacl from "tweetnacl"; // npm install tweetnacl
import { PublicKey, Keypair } from "@solana/web3.js";
import { Buffer } from "buffer"; // npm install buffer (browser polyfill)

// On air-gapped device — AirSign handles QR transport of keypair and message
// escrowPda: PublicKey from Step 1 | stealthPubkey: string from Step 2
// keypair: Keypair of the payer (never leaves the offline device)

const nonce = BigInt(1); // increment per payment
const lamports = BigInt(500_000_000); // 0.5 SOL

const nonceBuffer = Buffer.alloc(8);
nonceBuffer.writeBigUInt64LE(nonce);

const amountBuffer = Buffer.alloc(8);
amountBuffer.writeBigUInt64LE(lamports);

// 84-byte message: "MORA"(4) | escrow(32) | nonce(8) | payee(32) | amount(8)
const msg = Buffer.concat([
  Buffer.from("MORA"),
  escrowPda.toBuffer(),
  nonceBuffer,
  new PublicKey(stealthPubkey).toBuffer(),
  amountBuffer,
]);

const signature = nacl.sign.detached(msg, keypair.secretKey);
// AirSign encodes signature into fountain QR → online device scans

4. Settle MORA voucher (+ optional ZK pool routing)

Bob's online device scans the AirSign QR, recovers the signature, and settles the voucher. If ZK unlinkability is required, Bob deposits into a ZK shielded pool instead of receiving funds directly; the escrow still settles to the stealth address, which the pool then shields.

ts
import { Buffer } from "buffer";

// Bob's online device received the signature from AirSign QR scan.

// Step 1: Settle the MORA voucher on-chain.
// Ed25519 native verify + lamport transfer are atomic in one tx.
// Bob (settler) calls this — not Alice.
try {
  const settleTx = await program.methods
    .settle(Buffer.from(signature))
    .accounts({ settler: wallet.publicKey, escrow: escrowPda })
    .rpc({ commitment: "confirmed" });

  console.log("MORA settled:", settleTx);
} catch (err: unknown) {
  const msg = err instanceof Error ? err.message : String(err);
  if (msg.includes("already in use")) {
    throw new Error("Voucher already redeemed. Use a fresh nonce.");
  }
  throw err;
}

// Step 2 (optional): Route through ZK shielded pool for full unlinkability.
// Funds have now arrived at the stealth address — deposit into pool from there.
// Skip this block if ZK is not required.
const { txSignature: poolTx } = await zkPool.deposit({
  amount: Number(lamports),
  recipient: stealthPubkey, // stealth address from GrayBox (Step 2)
});
console.log("Shielded deposit:", poolTx);
After MORA settle: GrayBox quarantine flow. If the payee is a GrayBox stealth address, settled funds enter the quarantine vault and are held until the AML oracle attests. Bob then calls the release instruction on the quarantine-vault program to move funds to the treasury. See the GrayBox: How It Works section for the full flow.

Contact & Support

For integration support, partnerships, or enterprise inquiries: hello@siriuslabs.tech

Coming Next

Sirius AI: Private Payroll Agent

The first AI agent layer built on top of the Sirius stack. Offline authorization, stealth address routing, and air-gapped signing, orchestrated autonomously for private payroll flows on Solana. Coming 2026.