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.

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 and each closes a distinct attack surface. Today they run separately on devnet. The full-stack flow below is the target design: 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 (target design)

A full-stack payment is designed to follow this path. Steps 1 to 5 work today; step 6 is not wired yet.

  1. Bob publishes a GrayBox Meta-Address (spendPub + viewPub).
  2. Alice derives a one-time stealth address from Bob's Meta-Address.
  3. Alice builds an 84-byte MORA message that names the stealth address as payee. Her phone shows it as fountain-coded QR to her air-gapped AirSign signer, which signs it and returns the 64-byte signature the same way. No USB, no Bluetooth.
  4. Alice's phone shows the signed voucher to Bob as a QR code. Neither side needs a connection. Bob's app checks it offline: signature, escrow ownership, amount within the escrow, and his offline limit.
  5. When Bob is back online, he (or any relayer) submits Ed25519 verify + settle. A Receipt PDA is created for the nonce, so the same voucher can never settle twice. The SOL lands at the stealth address.
  6. Not wired yet: routing that payment through the GrayBox quarantine vault. MORA's settle pays the payee address directly; the vault only holds funds that arrive through its own deposit instruction. Connecting the two is on the roadmap.

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, Idl, Program } from "@coral-xyz/anchor";
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
import idl from "./mora.json"; // IDL from the MORA repo (see Installation)

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 as Idl, provider); // program ID comes from the IDL
  }, [connection, wallet]);
}

MORA: Offline Payment Escrow

DevnetAnchor 0.32Ed25519Pre-audit

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_escrow(seed, amount, expires_at)Lock SOL into a PDA derived from ["escrow", authority, seed]. Internet required only once.
settle(nonce, amount)Submit 2-instruction tx: Ed25519 native verify, then settle. Transfers lamports to the payee and creates the Receipt PDA ["receipt", escrow, nonce].
close_escrowAfter expires_at (set by the payer at creation), returns the unspent remainder and rent to the authority.

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 these guarantees on-chain. None of them requires the payer to be online at payment time.

AttackWhat an attacker triesHow MORA stops it
Double-spendSpend more than the escrow holds, by signing more vouchers than it can coverThe escrow tracks spent. settle only succeeds if amount <= amount - spent, otherwise it fails with InsufficientFunds. The escrow can never pay out more than was locked.
Double-settlementSubmit the same voucher on-chain twice (e.g. from two devices)Each nonce derives a unique Receipt PDA, created with an init constraint. A second settle with the same nonce fails with already in use 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, and settle checks that the signer is the escrow owner and the bytes match (SignerMismatch, MsgMismatch). If anything fails, the entire tx is rejected.
Late settlementSettle after the payer has reclaimed the escrowsettle fails with Expired after expires_at; only then can the payer call close_escrow.

Offline double-spend: how MORA bounds it

On-chain, an escrow can never pay out more than it holds. Offline, a payer could still sign vouchers that add up to more than the escrow, and then the first receiver to settle is paid. No offline system can rule this out without a connection, so MORA makes the exposure small and known in advance. A receiver accepts a voucher offline only if all of these hold:

text
accept offline  ⇔  Ed25519 signature valid
                   ∧ escrow = PDA("escrow", authority, seed)
                   ∧ amount ≤ escrow amount at creation
                   ∧ amount ≤ receiver's offline limit
                   ∧ now < expires_at

accept online   ⇔  the same checks against the live escrow
                   ∧ amount ≤ escrow.amount − escrow.spent
                   ∧ no Receipt PDA for this nonce yet

on-chain        ⇒  Σ settled ≤ escrow.amount   (always)

receiver's worst case  ≤  offline limit × vouchers accepted before reconnecting

The offline limit works like a card terminal's floor limit and is set by the receiver; it starts at zero, so offline acceptance is off until the receiver chooses a limit. Larger payments wait for a connection, where the voucher is checked against the live escrow before it is accepted. Short escrows and settling as soon as a connection returns keep the window small.

What a receiver can know offline

WhenWhat is known
Checked offlineThe escrow belongs to the signer (its address derives from the owner's key and seed). The owner signed exactly these 84 bytes, so payee, amount and nonce cannot be altered. The amount fits inside the escrow the voucher draws from.
Only known at settleWhether the funds are still there: a payer can promise the same escrow twice, and whoever settles first is paid. Whether settle lands before expires_at, after which the payer can reclaim the rest.
How the risk is boundedThe receiver's offline limit, an on-chain check whenever the receiver is online, short escrows, and settling as soon as a connection returns.

Status and limits

Devnet only, not audited. Settlement is in SOL; stablecoins are not wired yet. In the first phone test the signed voucher reached the receiver by copy and paste; phone-to-phone QR and Bluetooth handoff are next. Do not move real value through this program.

Proof on devnet

From the end-to-end test (2026-10-09) and the first payment from a real phone (2026-10-10). Every signature opens on Solana Explorer.

StepWhat happenedTransaction
create_escrowPayer locks SOL into the escrow PDA.3DvMBfYp… →
Ed25519 verify + settleReceiver submits the offline voucher.5nFdUsUy… →
Replay the same voucherRejected on-chain: the receipt PDA already exists (expected failure).3zkojVJZ… →
close_escrowAfter expiry, the remainder and rent return to the payer.5gMFyNgU… →
Phone: offline balanceAndroid app, approved in Solflare, locks 0.1 SOL into an escrow.4cWReqMt… →
Phone: offline payment collectedSigned in airplane mode; the receiver settled it later (0.01 SOL).2xKXYK9W… →
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 the IDL and save it as mora.json:
# https://github.com/sirius-labs-dev/mora/blob/main/landing/public/demo/mora.json

TypeScript SDK (@mora/sdk)

The same protocol code the Mora Android app runs: build, sign, encode and verify vouchers offline, and build the three escrow instructions. It works with the deployed program as is. The package is not on npm yet; wallet and POS teams can apply for SDK access. The examples on this page use only public packages and the IDL.

WhoWhenCall
Payeronline, oncecreateEscrowIx
PayerofflinebuildVoucherMessage → sign → encodeVoucherQr
ReceiverofflinedecodeVoucherQr → verifyVoucherOffline
Receiverback onlinesettleIxs → send
Payerafter expirycloseEscrowIx
ts
import {
  buildVoucherMessage, encodeVoucherQr, escrowPda,
  decodeVoucherQr, verifyVoucherOffline, settleIxs,
} from "@mora/sdk";

// Payer, offline: an Ed25519 signature over 84 bytes, no RPC
const escrow = escrowPda(payer.publicKey, seed);
const message = buildVoucherMessage({ escrow, nonce, payee, amount });
const signature = nacl.sign.detached(message, payer.secretKey);
const qr = encodeVoucherQr({ message, signature },
  { authority: payer.publicKey, seed, expiresAt, escrowAmount });

// Receiver: check offline, settle once back online
const voucher = decodeVoucherQr(qr);
const check = verifyVoucherOffline(voucher);
if (!check.ok) throw new Error(check.reason);
const tx = new Transaction().add(
  ...settleIxs(receiver.publicKey, voucher.meta!.authority, voucher));

TypeScript Types

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

interface MoraVoucher {
  escrow: PublicKey;
  nonce: bigint;         // u64, unique per payment
  payee: PublicKey;
  amount: bigint;        // lamports (u64)
  message: Uint8Array;   // the 84 signed bytes
  signature: Uint8Array; // 64-byte Ed25519 sig
}

// On-chain Escrow account, as returned by program.account.escrow.fetch()
interface EscrowAccount {
  authority: PublicKey;  // the payer, who signs vouchers
  seed: BN;
  amount: BN;            // lamports locked
  spent: BN;             // lamports already settled
  expiresAt: BN;         // unix seconds
  bump: number;
}

Escrow PDA Derivation

ts
import { PublicKey } from "@solana/web3.js";
import { Buffer } from "buffer";

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

const u64le = (n: bigint) => {
  const b = Buffer.alloc(8);
  b.writeBigUInt64LE(n);
  return b;
};

// Seeds: "escrow" | authority | seed (u64 LE)
function deriveEscrowPda(authority: PublicKey, seed: bigint): PublicKey {
  const [escrowPda] = PublicKey.findProgramAddressSync(
    [Buffer.from("escrow"), authority.toBuffer(), u64le(seed)],
    MORA_PROGRAM_ID
  );
  return escrowPda;
}

// Seeds: "receipt" | escrow | nonce (u64 LE). One per settled voucher.
function deriveReceiptPda(escrow: PublicKey, nonce: bigint): PublicKey {
  const [receiptPda] = PublicKey.findProgramAddressSync(
    [Buffer.from("receipt"), escrow.toBuffer(), u64le(nonce)],
    MORA_PROGRAM_ID
  );
  return receiptPda;
}

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 { Ed25519Program, Keypair, PublicKey, Transaction } from "@solana/web3.js";
import { Buffer } from "buffer"; // npm install buffer
import nacl from "tweetnacl";
// MoraVoucher: see TypeScript Types. u64le, deriveEscrowPda, deriveReceiptPda:
// see Escrow PDA Derivation. program: see useMoraProgram (Wallet Setup).

async function createEscrow(
  program: Program,
  authority: PublicKey,
  lamports: bigint,
  expiresInSeconds: number
): Promise<{ escrow: PublicKey; seed: bigint }> {
  const seed = BigInt(Date.now()); // any unique u64 per escrow
  const expiresAt = Math.floor(Date.now() / 1000) + expiresInSeconds;

  await program.methods
    .createEscrow(new BN(seed.toString()), new BN(lamports.toString()), new BN(expiresAt))
    .accounts({ authority })
    .rpc();

  return { escrow: deriveEscrowPda(authority, seed), seed };
}

function signVoucher(
  escrow: PublicKey,
  nonce: bigint,
  payee: PublicKey,
  amount: bigint,
  keypair: Keypair
): MoraVoucher {
  const message = Buffer.concat([
    Buffer.from("MORA"),
    escrow.toBuffer(),
    u64le(nonce),
    payee.toBuffer(),
    u64le(amount),
  ]); // 84 bytes

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

async function settleVoucher(
  program: Program,
  voucher: MoraVoucher,
  payer: PublicKey // escrow authority, the voucher signer
): Promise<string> {
  // The Ed25519 verify instruction must come right before settle.
  const verifyIx = Ed25519Program.createInstructionWithPublicKey({
    publicKey: payer.toBytes(),
    message: voucher.message,
    signature: voucher.signature,
  });
  const settleIx = await program.methods
    .settle(new BN(voucher.nonce.toString()), new BN(voucher.amount.toString()))
    .accountsPartial({
      submitter: program.provider.publicKey!, // anyone can submit
      escrow: voucher.escrow,
      payee: voucher.payee,
      receipt: deriveReceiptPda(voucher.escrow, voucher.nonce),
    })
    .instruction();

  const provider = program.provider as AnchorProvider;
  try {
    return await provider.sendAndConfirm(new Transaction().add(verifyIx, settleIx));
  } 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;
  }
}

Read the balance and close the escrow

ts
// What is still payable from an escrow, and until when
async function escrowBalance(program: Program, escrow: PublicKey) {
  const info = await program.provider.connection.getAccountInfo(escrow);
  if (!info) throw new Error("Escrow not found: it may already be closed");
  const e = program.coder.accounts.decode("escrow", info.data);
  const remaining = BigInt(e.amount.toString()) - BigInt(e.spent.toString());
  const expiresAt = Number(e.expiresAt.toString());
  return { remaining, expiresAt, expired: Date.now() / 1000 >= expiresAt };
}

// The payer reclaims the unspent remainder and rent
async function closeEscrow(program: Program, authority: PublicKey, escrow: PublicKey) {
  // Only after expires_at; otherwise the program returns NotExpired.
  return program.methods
    .closeEscrow()
    .accountsPartial({ authority, escrow })
    .rpc();
}

Error Handling

ts
try {
  await settleVoucher(program, voucher, payer);
} 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("SignerMismatch") || msg.includes("MsgMismatch")) {
    console.error("Voucher not signed by the escrow owner, or bytes altered");
  } else if (msg.includes("Expired")) {
    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;
  }
}

Error Codes

CodeNameWhen
6000ZeroAmountcreate_escrow or settle with amount 0
6001InvalidExpirycreate_escrow with expires_at in the past
6002Expiredsettle after expires_at
6003NotExpiredclose_escrow before expires_at
6004InsufficientFundssettle amount is more than amount − spent
6005SelfPaymentpayee is the escrow owner
6006Ed25519IxMissingno Ed25519 verify instruction right before settle
6007WrongVerifyProgramthe preceding instruction is not the Ed25519 program
6008MalformedSigEd25519 instruction data is malformed
6009SignerMismatchthe voucher was not signed by the escrow owner
6010MsgMismatchthe signed bytes do not match this escrow, nonce, payee and amount
6011Underflowarithmetic underflow (should not happen)
6012Overflowarithmetic overflow (should not happen)

A replayed voucher fails earlier, in the System Program, with already in use: its Receipt PDA already exists.

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

Mobile App (Android)

The Mora Android app runs the same protocol code on a phone. It is a preview build on devnet.

ScreenWhat it does
Offline balanceLoad an offline balance once: the app locks SOL into an escrow, approved in your wallet (tested with Solflare). When the connection drops, the app says so and keeps working.
Pay offlineScan the receiver's payment request, confirm, and show the signed code. No RPC call.
Receive and collectThe receiver checks the code offline against their offline limit, then the app settles it on Solana as soon as there is a connection.

Download the Android APK (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: A payment is deposited into an on-chain quarantine vault, recorded against a fresh stealth address. AML oracles, permissioned off-chain services, submit on-chain attestations; the vault needs min_attestations of its oracle set (M-of-N) before the deposit's release_authority can call release to a treasury address. A dirty verdict or an expired deposit goes back to the depositor through refund. In the current devnet implementation, Sirius Labs operates the oracle; the design supports third-party AML providers. Experimental and unaudited.

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 (x25519 ECDH)
3AliceDeposits funds into the quarantine vault, recorded against the stealth address
4IndexerScans chain with Bob's view key, detects deposit, notifies gateway
5AML OraclesAttest the funds (M-of-N). Clean: the quarantine gate opens. Dirty: refund.
6Release authorityReleases funds to a fresh treasury address, or refunds the depositor

Steps 3, 5, and 6 are on-chain Anchor program calls (deposit, attest, release / refund). 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
}

Live Dashboard →

Live on mainnet: GrayBox Agent Checkout for PayBox

Solana mainnetUSDCPayBox pluginOperator-held

GrayBox Agent Checkout is our plugin for the PayBox (MoonPay) marketplace. An AI agent buys from a merchant in USDC; every payment is AML-screened before the merchant sees it. Clean payments are released to the merchant's one-time address, so the merchant's treasury never shows up next to the buyer. Anything else is refunded.

ToolWhat it does
list_merchantsThe businesses that accept agent payments, their items, prices and token.
screen_addressScreen any Solana wallet against the sources below and return the evidence.
checkoutBuild an unsigned USDC payment for an item; PayBox signs it in the agent's wallet.
checkout_statusScreen the payer, then release the payment to the merchant or refund it, once.

Screening uses public sources: the OFAC SDN list's Solana addresses, Circle (USDC) and Tether (USDT) freeze state on mainnet, and the wallets that funded the payer. If a source cannot be read, the verdict is incomplete and the money stays put; it is never released. Each settlement writes the verdict and an evidence hash to an on-chain memo.

How mainnet differs from devnet. On mainnet the payment is held in an operator wallet run by Sirius Labs, not in the quarantine-vault program, and released or refunded from there. The program-based, non-custodial vault runs on devnet. Sanctions screening is narrow (the OFAC list has 4 Solana addresses today); wider risk scoring needs a provider such as Chainalysis or TRM, which the vault can take as a second oracle.

Proof on mainnet

Real purchases made by an AI agent through PayBox on 2026-10-17, 0.01 USDC each. The flagged payments come from a demo wallet placed on the operator list, because nobody can hold a sanctioned wallet's key; screening a real OFAC-listed address with screen_address flags it on the OFAC list and on its frozen USDC and USDT accounts.

StepWhat happenedTransaction
Clean paymentAgent pays 0.01 USDC from a clean PayBox wallet (EkQnUwWv…).h2nwWvaY… →
ReleasedVerdict clean: released to the merchant's one-time address.4rLFDU32… →
Flagged payment0.01 USDC from the demo flagged wallet (X3ASdmoy…, a test entry in the operator list).4EXDe8Bz… →
RefundedVerdict dirty: refunded to the payer; the merchant never receives it.4znWFsXD… →
Second clean paymentAnother 0.01 USDC from the clean wallet.51yWF92p… →
ReleasedReleased once.5AQv7xwi… →
Duplicate releaseA concurrent second release of the same payment fails: its settlement marker account is already in use (expected failure).Rn2xoygv… →

Merchant panel and live screening →

Quick Start: Full Stack Integration

End-to-end flow: MORA escrow (online, once) → GrayBox stealth address → AirSign air-gapped signing → MORA settle. The SOL lands at the stealth address; routing it through the GrayBox quarantine vault is not wired yet.

1. Fund escrow: MORA (online, once)

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

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

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 as Idl, provider);

    const seed = new BN(Date.now());
    const expiresAt = new BN(Math.floor(Date.now() / 1000) + 7 * 24 * 3600); // 7 days

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

    const [escrowPda] = PublicKey.findProgramAddressSync(
      [Buffer.from("escrow"), wallet.publicKey.toBuffer(), seed.toArrayLike(Buffer, "le", 8)],
      program.programId
    );
    console.log("Escrow funded:", escrowPda.toBase58());
    // Save escrowPda and seed: every voucher names the escrow
  }

  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

Bob shares his Meta-Address (spendPub + viewPub). Alice derives a one-time stealth address from it with x25519 ECDH (see GrayBox: How It Works). The result, stealthPubkey, is the payee in the voucher below.

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: PublicKey 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,
  stealthPubkey.toBuffer(),
  amountBuffer,
]);

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

4. Settle MORA voucher

Bob's online device scans the AirSign QR, recovers the signature, and settles the voucher.

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

// Bob's online device received msg + signature from the AirSign QR scan.
// alicePubkey: escrow authority | nonce, lamports: from Step 3

const [receiptPda] = PublicKey.findProgramAddressSync(
  [Buffer.from("receipt"), escrowPda.toBuffer(), nonceBuffer],
  program.programId
);

// Ed25519 native verify + settle are atomic in one tx.
// Anyone can submit: here Bob pays the fee.
const settleIx = await program.methods
  .settle(new BN(nonce.toString()), new BN(lamports.toString()))
  .accountsPartial({
    submitter: wallet.publicKey,
    escrow: escrowPda,
    payee: stealthPubkey,
    receipt: receiptPda,
  })
  .instruction();

try {
  const settleTx = await provider.sendAndConfirm(new Transaction().add(
    Ed25519Program.createInstructionWithPublicKey({
      publicKey: alicePubkey.toBytes(),
      message: msg,
      signature,
    }),
    settleIx
  ));
  console.log("MORA settled:", settleTx);
} catch (err: unknown) {
  const m = err instanceof Error ? err.message : String(err);
  if (m.includes("already in use")) {
    throw new Error("Voucher already redeemed. Use a fresh nonce.");
  }
  throw err;
}
After MORA settle. Settle pays the stealth address directly. It does not pass through the GrayBox quarantine vault: the vault only holds funds that arrive through its own deposit instruction, and connecting the two is on the roadmap. See GrayBox: How It Works for the vault flow on its own.

Contact & Support

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