On-chain payroll is public by default. Veil makes it private without breaking auditability.
Chainlink Convergence 2026 — Privacy Track
Every Gnosis Safe payroll transaction is visible on Etherscan the moment it lands. Open any DAO multisig — Uniswap, Optimism Collective, Gitcoin — and you can read exactly who got paid, how much, and when. The recipient's wallet address links to their entire transaction history. Their landlord, their next employer, and every phishing bot on the network can see it too.
This is not a hypothetical. DAO contributors have faced targeted phishing and physical threats tied directly to on-chain salary data. There is no existing payroll tool for Web3 — Utopia, Coordinape, Superfluid — that addresses this. They all settle on a public chain, in the clear.
Labor law in most jurisdictions treats payroll data as confidential. Web3 payroll is structurally non-compliant by design.
- Executes payroll privately. The CRE workflow fetches recipient addresses and amounts from an off-chain HR API via Confidential HTTP (TEE-encrypted — node operators cannot read the response). Transfers go through Chainlink ACE with
hide-senderflag and shielded recipient addresses. Individual amounts never appear on the public chain. - Enforces budget on-chain without exposing individual payments.
VeilVault.soltracks monthly spend limits, batch lifecycle (PENDING → EXECUTING → COMPLETED), and role-based access. The public chain sees aggregate totals only. - Proves correctness without revealing details. After each batch, the CRE workflow submits a
PrivacyAttestationto the vault: a Merkle root over all(recipient, amount)leaves, a sum-check hash, and an IPFS pointer to an encrypted audit report. Any employee can prove they were paid — without seeing anyone else's amount.
| Capability | How It's Used | Code Reference |
|---|---|---|
| CRE (Runtime Environment) | Cron-triggered workflow orchestrates the full 7-step payroll pipeline. Reads contract state, writes batch lifecycle transitions, coordinates all other capabilities. | workflow/main.ts — onCron() |
| CRE Secrets | runtime.getSecret({ id: 'TREASURY_PRIVATE_KEY' }) fetches the treasury signing key at runtime inside the TEE. Never stored in config. Node operators cannot see it. |
workflow/main.ts — privateKey |
| Confidential HTTP | The HTTPClient callback wraps both the payroll API fetch and all ACE transfer requests. The full envelope — URL, headers, request body, response body — is encrypted. Recipient addresses and amounts are invisible to the Chainlink network. |
workflow/main.ts — executePayroll() |
| Chainlink ACE (Confidential Compute) | Private token transfers with hide-sender flag, EIP-712 authorization, and shielded address generation per recipient. Transfers do not appear as individual on-chain events. |
workflow/main.ts — executePayroll(), signACE() |
| Data Feeds | latestRoundData() on ETH/USD and LINK/USD feeds at LAST_FINALIZED_BLOCK_NUMBER. Used to convert payroll amounts to USD for budget validation before execution. |
workflow/main.ts — Step 2 |
| CCIP | Recipients with a chain field in payroll data are routed cross-chain. executePayroll() collects them as crossChainRecipients; onCron() calls VeilVault.sendCrossChainPayroll(batchId, chainSelector, recipient, token, amount) per recipient. Config: ccipDestinations with Ethereum Sepolia chain selector 16015286601757825753. |
workflow/main.ts — Step 5c, VeilVault.sol:378 |
| What Goes On-Chain (Public) | What Stays Off-Chain or Sealed |
|---|---|
| Batch ID, batch type (PAYROLL / GRANT / TREASURY_OP), recipient count, total amount, memo | Individual recipient addresses |
| Batch status transitions: PENDING, EXECUTING, COMPLETED | Individual payment amounts |
Merkle root of all (recipient, amount) leaves |
Payroll API credentials (CRE Secrets, TEE) |
Sum-check hash: keccak256(sum_of_parts, totalAmount) |
Full payroll API response (Confidential HTTP) |
| IPFS hash of encrypted audit report | Plaintext audit report (treasurer-only decryption key) |
| Monthly budget consumed (aggregate) | Which wallet received which amount |
Cron Trigger
|
v
STEP 1: Read pending batches from VeilVault (EVM read, Base Sepolia)
|
v
STEP 2: Fetch ETH/USD + LINK/USD from Chainlink Data Feeds
|
v
STEP 3: Confidential HTTP envelope opens
| |
| +-- GET payroll API --> recipient list + amounts (encrypted in transit)
| |
| +-- ACE: check treasury balance (EIP-712 signed, hide-sender)
| |
| +-- For each recipient:
| +-- ACE: generate shielded address
| +-- ACE: private transfer with hide-sender flag
|
Confidential HTTP envelope closes (node operators saw nothing)
|
v
STEP 4: Mark batch EXECUTING on VeilVault (EVM write, onlyCRE)
|
v
STEP 5: Validate batch total against on-chain monthly budget
|
v
STEP 5c: CCIP routing -- for recipients with chain field, call sendCrossChainPayroll()
| (Ethereum Sepolia selector: 16015286601757825753)
v
STEP 6: completeBatch() on VeilVault -- updates budget, emits aggregate event
|
v
STEP 7: submitAttestation() -- Merkle root + sum-check hash + IPFS audit hash
VeilVault — Base Sepolia
0x42118F942332f6cAEE0aA768bB741A2f1eb65204
For VaultFactory deployment details, see the deployment section in contracts/script/Deploy.s.sol.
Key functions:
| Function | Access | What It Does |
|---|---|---|
submitBatch() |
Payroll Admin | Registers a new batch with total amount + data hash of encrypted off-chain details |
markBatchExecuting() |
CRE workflow only | Locks batch before transfers begin |
completeBatch() |
CRE workflow only | Records actual amount spent, updates monthly budget |
submitAttestation() |
CRE workflow only | Stores Merkle root + sum-check hash + IPFS audit pointer |
verifyPaymentInclusion() |
Anyone | Verifies a single payment against the stored Merkle root |
sendCrossChainPayroll() |
CRE workflow only | CCIP cross-chain distribution (implemented, config-ready) |
Role hierarchy: Owner > Treasury Manager > Payroll Admin > CRE Workflow (contract-to-contract only).
This is the core privacy-vs-accountability tradeoff, resolved on-chain.
After ACE transfers complete, the CRE workflow builds a Merkle tree where each leaf is:
leaf = keccak256(abi.encodePacked(recipientAddress, amount))The root is stored in VeilVault via submitAttestation(). The sum-check hash keccak256(abi.encode(sum_of_parts, totalAmount)) proves no funds were skimmed or misrouted — the sum of all individual payments equals the batch total declared on-chain.
An employee who wants to verify they were paid calls:
vault.verifyPaymentInclusion(
batchId, // which batch
myAddress, // my wallet address
myAmount, // amount I claim to have received
merkleProof // proof path provided by the treasurer
)This returns true and emits PaymentVerified(batchId, recipient, amount) if the proof is valid.
What this achieves: the employee proves inclusion without the contract revealing any other leaf. No other employee's address or amount is exposed. The treasurer provides individual proofs off-band (e.g., via encrypted email). The DAO can publicly assert "all 12 contributors were paid" and any contributor can independently verify their own payment — on-chain, trustlessly — without the full payment list ever being public.
Dispute resolution becomes possible: if a contributor claims non-payment, the treasurer provides a Merkle proof. If no valid proof exists, the on-chain attestation proves the batch was processed without that recipient.
Prerequisites: Bun, Foundry, CRE CLI
# Clone
git clone https://github.com/your-org/xchain-agents
cd veil
# Install workflow dependencies
cd workflow && bun install
# Build contracts
cd ../contracts && forge build
# Run tests (32/32 pass)
forge test -vv
# Simulate CRE workflow
cd ..
cre workflow simulate workflow -T staging-settings
# Run frontend
cd frontend && bun install && bun run devDeploy to Base Sepolia:
cd contracts
PRIVATE_KEY=0x... forge script script/Deploy.s.sol:DeployVeilVault \
--rpc-url https://sepolia.base.org --broadcast --legacyforge test -vv
Running 32 tests for test/VeilVault.t.sol:VeilVaultTest
[PASS] testConstructor()
[PASS] testSubmitBatch()
[PASS] testSubmitBatchAsAdmin()
[PASS] testSubmitBatchRevertUnauthorized()
[PASS] testSubmitBatchRevertExceedsBudget()
[PASS] testSubmitBatchRevertZeroRecipients()
[PASS] testSubmitBatchRevertZeroAmount()
[PASS] testMarkBatchExecuting()
[PASS] testMarkBatchExecutingRevertNotCRE()
[PASS] testCompleteBatch()
[PASS] testCompleteBatchRevertNotExecuting()
[PASS] testCancelBatch()
[PASS] testCancelBatchRevertNotPending()
[PASS] testBudgetTracking()
[PASS] testBudgetResetsAfterMonth()
[PASS] testBudgetBlocksOverspend()
[PASS] testGetPendingBatches()
[PASS] testTotalBatches()
[PASS] testSetMonthlyBudget()
[PASS] testSetMonthlyBudgetRevertNotOwner()
[PASS] testSetTreasuryManager()
[PASS] testSetCREWorkflow()
[PASS] testFullPayrollCycle()
[PASS] testGrantDistributionCycle()
[PASS] testTreasuryOpCycle()
[PASS] testSubmitAttestation()
[PASS] testAttestationRevertNotCompleted()
[PASS] testAttestationRevertDuplicate()
[PASS] testAttestationRevertNotCRE()
[PASS] testVerifyPaymentInclusion()
[PASS] testVerifyPaymentInclusionInvalid()
[PASS] testFullCycleWithAttestation()
32 passed, 0 failed
| Layer | Stack |
|---|---|
| Workflow | TypeScript, @chainlink/cre-sdk ^1.1.2, Bun |
| Smart Contracts | Solidity 0.8.24, Foundry |
| ACE signing | @noble/curves (secp256k1, EIP-712) |
| ABI encoding | viem |
| Config validation | zod |
| Frontend | Next.js 15, wagmi v3, viem, Tailwind CSS |
Chainlink Convergence 2026 — Privacy Track
Veil is a submission for the privacy track. The core claim: six Chainlink capabilities (CRE, CRE Secrets, Confidential HTTP, ACE, Data Feeds, CCIP) can be composed into a system where a DAO pays its contributors privately, proves correctness on-chain, routes cross-chain payments automatically, and gives every recipient a way to verify their own payment — without a trusted intermediary and without making salary data public.
The demo scenario is employee payment verification: a contributor to "Veil Demo DAO" receives a Merkle proof from the treasurer after the March 2026 payroll batch completes. They call verifyPaymentInclusion() on Base Sepolia and get an on-chain confirmation they were paid, with no other salary data exposed.