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’sfrom.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:- You log in. The browser runs SEP-10 against
payroll-api: it fetches a challenge fromGET /auth/sep10/challenge?account=G..., signs it with your wallet, and posts it toPOST /auth/sep10/verify. The API returns a session JWT whosesubis yourG-address. The challenge is single-use and expires in 5 minutes. - 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. Theconfidential_transfercall carries no plaintext amount. - You sign the envelope. Your wallet signs the inner transaction with a plain
signTransaction. You are the transaction source, so source-account auth satisfiesfrom.require_auth(). The signature never leaves as a secret: only the signed transaction travels. - The browser posts
{ signedXDR }. The request goes toPOST /transferwith your session JWT in theAuthorization: Bearerheader. - The API checks the binding.
requireSessionvalidates the JWT, then the route assertsinner tx.source == session.sub. If you try to fee-bump someone else’s transaction, the API returns403. A Privy session cannot use this route and returns501; only SEP-10 sessions fee-bump. - 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. - 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 by0 XLM.
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.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
Why a BFF instead of calling Soroban RPC straight from the browser?
Why a BFF instead of calling Soroban RPC straight from the browser?
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.Does the server ever see an amount or a secret?
Does the server ever see an amount or a secret?
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.What is the difference between the SDP relay and the Fork B path?
What is the difference between the SDP relay and the Fork B path?
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.