Skip to main content
Confiroll is a pnpm/apps/ monorepo. The design principle behind everything below: the confidential work happens in the browser, and the server stays thin. Confiroll’s backend holds no user keys. It authenticates sessions, fee-bumps already-signed transactions, and relays them to the chain. Two live hosts run the deployment: the payroll-api BFF at https://api.confiroll.com, and the headless SDP at https://sdp.confiroll.com. Everything settles on the Stellar testnet.

The pieces

Web app (apps/web)

Vite + React + TypeScript + Tailwind + shadcn. Sign-in with Stellar Wallets Kit (SEP-10) or Privy (email). This is where confidential amounts are encrypted, proofs are built, and balances are decrypted.

payroll-api (apps/payroll/api)

A Fastify BFF (backend-for-frontend). Issues session JWTs, and exposes the core POST /transfer that fee-bumps a browser-signed confidential transfer. Holds operational keys only, never yours.

Confidential pipeline (apps/payroll/client)

The TypeScript toolkit and vendored ctd-sdk (OpenZeppelin confidential-token crypto + Noir/bb.js proving) that implements register, deposit, transfer, merge, and withdraw, plus the fee-bump sponsor.

Soroban contracts (contracts/)

payslip-anchor (tamper-evidence), cctp-vault (bridged-USDC landing point), and passkey-wallet (seedless WebAuthn account). Deployed on testnet.

Headless SDP (apps/sdp)

A vendored Stellar Disbursement Platform (v6.6.1) used only as a channel-account pool
  • fee-bump + submit + retry, not its disbursement API. One small upstream patch.

Tools (tools/)

Supporting CLIs: CCTP burn, Wallets-Kit signing helpers, cost and network-settings utilities.

Repository layout

Every part of the system maps to one directory in the monorepo. When you read the source, this is where each responsibility lives:
apps/web is the only part a signed-in person touches directly. apps/payroll/client and apps/payroll/vendor/ctd-sdk carry the confidentiality logic: the same proving code runs from CLI scripts on the testnet today and from the browser in the app.

How a confidential payout flows

The core write path (Fork B) keeps the employer’s keys in the browser and lets Confiroll sponsor the fee without ever holding a secret: The employer signs the inner transaction as its source account, so the confidential token’s from.require_auth() is satisfied by a plain signature, with no fragile auth-entry signing. Confiroll wraps that signed transaction in a fee-bump and pays the outer fee, so the employer spends 0 XLM. The amount is never an argument. It lives inside the proof. See The non-custodial model for why this “Fork B” design is the pivotal decision.

Request lifecycle: one Fork B transfer

Read the diagram above as a story. Here is the same path in prose, so you can see where each guarantee comes from:
  1. You log in. The browser runs SEP-10 against payroll-api: it fetches a challenge from GET /auth/sep10/challenge?account=G..., signs it with your wallet, and posts it to POST /auth/sep10/verify. The API returns a session JWT whose sub is your G-address. The challenge is single-use and expires in 5 minutes.
  2. The browser builds the transfer. Using your viewing key sk, the client encrypts the amount to the recipient’s public viewing key and generates the zero-knowledge proof with bb.js. The confidential_transfer call carries no plaintext amount.
  3. You sign the envelope. Your wallet signs the inner transaction with a plain signTransaction. You are the transaction source, so source-account auth satisfies from.require_auth(). The signature never leaves as a secret: only the signed transaction travels.
  4. The browser posts { signedXDR }. The request goes to POST /transfer with your session JWT in the Authorization: Bearer header.
  5. The API checks the binding. requireSession validates the JWT, then the route asserts inner tx.source == session.sub. If you try to fee-bump someone else’s transaction, the API returns 403. A Privy session cannot use this route and returns 501; only SEP-10 sessions fee-bump.
  6. The sponsor validates before it signs. It checks the contract allow-list, the fee cap, and the per-account quota. If it refuses, the API returns 422. If it accepts, it builds a CAP-15 fee-bump with an outer fee of twice the inner fee, signs the bump, and submits over Soroban RPC.
  7. The chain settles. The confidential token executes the transfer. The RPC polls to SUCCESS, and the API returns { hash, status: "SUCCESS" }. The fee account on-chain is the sponsor, and your account moves by 0 XLM.
The sponsor never needs the employer’s secret. It only pays the outer fee. That single fact, plus browser-side proving, is what makes the path non-custodial and gas-free at the same time.

What runs where

Design principles

Three rules explain almost every choice in the codebase:

The browser does the confidential work

Encryption, proving, and decryption all need your viewing key sk. That key stays on your device, so the confidential logic runs client-side in apps/payroll/client and vendor/ctd-sdk.

The server stays thin

payroll-api authenticates sessions and fee-bumps signed transactions. It does not prove, does not decrypt, and does not decide amounts. Its job is small on purpose.

Confiroll holds zero user keys

Neither your Stellar signing key nor your confidential sk reaches a Confiroll server. The only keys Confiroll holds are operational: the fee sponsor, the testnet USDC faucet, and SDP’s channel accounts.
Because those three hold, amounts stay hidden from the public, from other employers, and from Confiroll itself. That property has a name in these docs: Confiroll-blind.

Keep reading

The ecosystem

Where Confiroll sits among Stellar, Soroban, the confidential token, SDP, and Circle CCTP.

The non-custodial model

Two secrets per user, browser-side proving, and why Fork B is the pivotal decision.

Confidential token flow

Register, deposit, transfer, merge, and withdraw, and exactly what is public vs hidden.

Security and claims

The confidentiality-not-anonymity boundary and the live-vs-designed line.

FAQ

The browser could reach an RPC on its own, but then the employer would pay the fee and there would be no fee sponsorship. The BFF exists so Confiroll can fee-bump your signed transaction (you spend 0 XLM) while still never holding your key. It also holds the SDP database credentials internally and never exposes them to the client.
No. The browser sends a signed transaction whose amount lives inside a proof and encrypted event fields, never a plaintext argument. sk is derived and used only on your device. The server sees a session JWT and a signed transaction, nothing more.
They are two different write mechanisms. The deployed non-custodial path is Fork B: POST /transfer fee-bumped by the API sponsor. The SDP queue-relay (writing a row into tss.submitter_transactions) powers the batch path. See SDP integration for the relay and Fee sponsorship for the sponsor.
The confidential write path is proven end to end on Stellar testnet via the client scripts in apps/payroll/client, and this page describes that architecture. See Security & claims for the live-vs-designed boundary.