Begin
Start here
one SDK object · one public API · a template that runs as-is
Import the pool module from dapp/ in the repository and you have the read path, every op builder and the relay client. Nothing needs a build step, a framework or a backend. docs/dapp-template/index.html is a one-file starting point that opens in a browser and runs.
import { makeConfidentialPoolUx } from './dapp/confidential-pool-ux.js';
import * as secp from '@noble/secp256k1';
import { keccak_256 as keccak256 } from '@noble/hashes/sha3';
import { sha256 } from '@noble/hashes/sha256';
const tacit = makeConfidentialPoolUx({ secp, keccak256, sha256, network: 'mainnet' });
const acct = tacit.account(walletPriv); // the derived Ethereum account: { address, priv }
const me = tacit.identity(walletPriv); // the confidential identity: { priv, pubHex, owner, secret }
const { notes, byAsset } = await tacit.balance(walletPriv);Before your first op
- One secp256k1 key is the identity.
tacit.account(walletPriv)is the Ethereum account that sends your deposits and self-submitted settles. It is not the plain address of the same key. Fund it with a little ETH once; relayed ops need none. - Everything comes back from the key.
tacit.recover({ walletPriv })finds notes, farm and CDP positions, cBTC and stealth locks from chain data alone. Save the few records it cannot re-derive, such as a CDP position’s secrets, before you submit. - Rescan before a spend. The pool accepts every root it has ever had, so a membership path never expires. An op carries one
spendRoot, so take every input’s path from the same scan. - One key per process. A scan returns every note sealed to the key, including notes another process made.
- A first integration: wrap, settle the wrap, send to yourself, unwrap. That covers a deposit, a proof, a spend and an exit.
| Op | SDK call | Relay type | How it lands | Path |
|---|---|---|---|---|
| Deposit ETH or a token | wrap, then submitWrapSettle | wrap | The deposit is your own transaction; its settle is relayed free | 01 |
| Deposit and split at once | wrapAndSend | wraptransfer | The relay proves; you send the router transaction | 01 |
| Send to yourself, split, merge | transfer | transfer | Relayed, fee at or above the floor | 01 |
| Pay someone else | stealthSend, stealthClaim, stealthRefund | stealthlock, stealthclaim, stealthrefund | Relayed | 01 |
| Exit to an address | unwrap, sendUnwrap | unwrap, sendunwrap | Relayed, fee at or above the floor | 01 |
| Exit to Base or Robinhood Chain | exit builders in confidential-router.js | unwrap, then activateExit | A relayed unwrap and a permissionless activation, or one exitAndExecute | 01 |
| Swap or route | route, quoteRoute | route | Relayed | 02 |
| Add or remove liquidity | lpAdd, lpRemove | lp, lpremove | Relayed; founding a pool is self-submitted | 02 |
| Bitcoin to Ethereum | bridgeBurnToPool, then bridgeMint | bridgemint | A Bitcoin burn, then a relayed mint (free at fee 0) | 03 |
| Ethereum to Bitcoin | crossOut, then completeCrossOutOnBitcoin | bridgeburn | A relayed burn, then a Bitcoin commit and reveal | 03 |
| Spend a Bitcoin-homed note on Ethereum | fastlaneExit | fastlane | Self-submitted by default | 03 |
| Lock BTC, mint cBTC | cbtc-lock.js, CbtcEscrowHelper, mintCbtc | cbtcmint | Relayed, free | 04 |
| Borrow and repay cUSD | defiActions(walletPriv).openCdp, closeCdp, topupCdp | cdpmint, cdpclose, cdptopup | Relayed | 04 |
| Farm | lpBond, farmBond, farmHarvest, farmUnbond, farmRedeem | lpbond, farmbond, farmharvest, farmunbond | Relayed; lpBond is self-submitted | 05 |
Begin
The model
one settle function · one immutable program key
- Your balance is a set of notes. A note is
(asset, value, blinding, owner); only its Pedersen commitmentC = value·H + blinding·Gis public. - The pool stores leaves,
keccak(assetId ‖ Cx ‖ Cy ‖ owner), in an append-only Merkle tree. - Spending a note publishes a nullifier, never the leaf. A second spend reverts.
- Every change goes through
ConfidentialPool.settle(publicValues, proofBytes, memos), which verifies one SP1 Groth16 proof against the pool’s immutablePROGRAM_VKEY.wrap()is the one exception: a plain payable call that escrows funds. - A memo per created leaf carries the note’s opening, encrypted to its owner. Notes a wallet mints for itself also re-derive from its key.
- The relay proves and settles an op for a relay fee paid out of that op. It never receives the wallet key or a note’s blinding.
The pool has no proxy, no upgrade path, no pause switch and no key over escrow, exits or payouts. It is the root of the V1 lineage. Its lineage steward, the ops multisig, can deploy a successor once; that closes this pool to new value while every exit stays open. pool.successor() reads zero.
Four things that surprise everyone
- You cannot send a note straight to someone else. A note’s owner is
keccak(nk ‖ dom), a hash, so a note minted to another person’s key could never be spent. Pay others with a stealth lock that they claim. - Values are 8-decimal in-system units, not wei. The value is
amount / unitScaleand must divide exactly; ETH, TAC, cBTC and cUSD use1e10. - Bitcoin value arrives two ways. A bridge burn is minted once on Ethereum, or a note bound to this pool is spent directly in the fast lane. Both wait for reflection, which folds a Bitcoin block once it is 24 blocks deep, about four hours.
- A wrap is two steps. The deposit is a plain transaction; a proof turns it into a note.
Path 01
ETH and tokens · wrap, send, exit
ETH 0x3cba71e1…6f34 · any registered ERC-20 · no Bitcoin involved
// 1. deposit: your own transaction, no proof
const w = await tacit.wrap({ walletPriv, amountWei: 10n ** 16n, ticker: 'cETH' });
// 2. turn the deposit into a note: one relayed proof, once the wrap is mined
await tacit.submitWrapSettle({ built: w });
// send to yourself (split or merge), pay someone else, exit
const fee = await tacit.quoteTransferFee(5_000_000n, 'cETH');
await tacit.transfer({ walletPriv, notes, recipientPubHex: me.pubHex, amount: 5_000_000n, fee });
await tacit.stealthSend({ walletPriv, notes, recipientPubHex, amount: 5_000_000n });
await tacit.unwrap({ note: notes[0], walletPriv, recipient: '0x…' });- Record the wrap index.
tacit.wraptakes the next unused index. If the settle step fails,buildWrapwith the same index rebuilds the same deposit. The escrow has no deadline, and wrapping again would deposit twice. - One transaction.
wrapAndSendwraps and settles together and splits the deposit into an amount and change. To pay the relay inside a plain deposit, send it throughWrapTipForwarderwith the tipGET /confidential/quoterecommends. - Paying someone else.
stealthSendlocks to a one-time key derived from theirtacit1…address, and they claim it. The sender can refund after the lock’s deadline, about 90 days by default. A.wei,.gweior.ethname can carry that address in itsfinance.tacittext record;confidential-names.jsresolves and publishes it. - Exiting.
unwrappays a whole note out;sendUnwrappays an exact amount and keeps the change private. Confirm an exit by the recipient’s balance or the settle receipt, not by the relay’s status. The pool has nowithdraw()and noreceive(). An exit is theunwrapop, a proof thatsettleapplies. A direct call or a plain transfer to the pool address reverts and moves nothing.
Exiting straight to Base or Robinhood Chain
ConfidentialRouter unwraps into a deterministic escrow (escrowAddressFor(recipe)) and makes the L2 deposit from it. exitAndExecute(recipe, publicValues, proofBytes, memos) does it in one transaction you send. Otherwise a relayed unwrap funds the escrow and anyone calls activateExit(recipe). reclaimExit returns the funds if the deposit never fires. Base uses L1StandardBridge. Robinhood Chain, an Arbitrum Orbit chain, uses a retryable ticket.
- Arbitrum aliases contract callers: use
createRetryableTicketnaming the recipient, and checketh_getCodeon any refund address. exitAndExecuteneeds about 2,000,000 gas andactivateExit800,000 to 1,000,000. Cap a dry run at the limit you will send.
mainnetrelayed send-and-unwrap in cETH and cUSDT · a relayed wrap settle, transfer and unwrap · wrap-and-send · stealth lock and claim
Path 02
Trading and liquidity
one (asset pair, fee tier) is one pool · public reserves · anyone can found one
const poolId = tacit.routePoolId(assetA, assetB, 30);
const res = await tacit.poolReserves(poolId); // null if never founded
const q = await tacit.quoteRoute({ asset0: assetA, amountIn, path: [{ assetNext: assetB, feeBps: 30 }] });
await tacit.route({ walletPriv, inNote, amountIn, path: [{ assetNext: assetB, feeBps: 30 }], minOut });
const add = await tacit.quoteLpAdd({ assetA, assetB, feeBps: 30, amountA });
await tacit.lpAdd({ walletPriv, aNote, bNote, feeBps: 30, fee: add.fee });
await tacit.lpRemove({ walletPriv, assetA, assetB, feeBps: 30, shareNote, fee: null });- Whole notes. A route spends its input note whole, up to four hops in one settle, and
lpRemoveburns a whole share note. Split withtransferfirst to use part of one. - Founding. The first add goes through
createPairAndSettlefrom your own account (lpAddwithselfRelay: true). A relayed first add revertsPoolNotInit. The founding amounts set the price, and 1,000 shares stay locked for good. - In ratio. After founding, add the amount
quoteLpAddreturns. An off-ratio add donates the difference to existing LPs. - Finding a pool. There is no registry or creation event. Compute the id and read
pools(poolId). The dapp probes the 30, 5, 100 and 1 bps tiers.
The public face: TacitPublicAmm
TacitPublicAmm trades the same reserves with plain ERC-20s and ETH, with no proof and no relay. quoteSwap(assetIn, assetOut, feeBps, amountIn) returns exactly what swapPublic(assetIn, assetOut, feeBps, amountIn, minAmountOut, deadline, to) pays, and amounts must be multiples of 1e10. ConfidentialRouter adds permit and Permit2 entry points: swapPublicWithPermit, swapPublicExactIn, swapPublicPathWithPermit2, swapPublicETHPath, addLiquidityPublicWithPermit2 and addLiquidityPublicETHWithPermit2. This is where an aggregator plugs in.
| Pair | Pool id | LP-share asset id |
|---|---|---|
| TAC / cETH | 0x248497bf6f943cd2b39a04bf5841056c58dfd7ef196188cb4f0ac1fd11dc7c00 | 0x17c56713a7e4a5d679a71def3ff9fa186f1556ef757b0ee6b7a3ed8c9249ef99 |
| cETH / cUSD | 0x5925c0c2954c5b11bedd20e444f0cb22f3827f6197814f97d193e7a44e909da7 | 0xd608b0c3806e782cc213e2d52245c3c2fbef455a10410a1f5ccc61ba45262571 |
| cETH / cBTC | 0x8359cd1f812e3a9040129e186e1477f60f2188bad1bd6a56e5506b22bf5cd331 | 0x0a0cce175bc483945822c8de3d3926e813269f5e1bbe1f9c853e09ed48b68254 |
mainnetTAC/cETH founding · relayed swap, LP add and LP remove · a two-hop route, cETH → TAC → cUSDT, in one settle
Path 03
Bridge · Bitcoin and Ethereum
TAC, tETH, cBTC and cUSD ride one bridge · REFLECTION_CONFIRMATIONS = 24
Every canonical asset bridges the same way; only the asset id differs. tETH is native ETH under its Bitcoin-side id, so a bridged tETH note and a wrapped cETH note are one asset. GET /reflection/status shows the Bitcoin height reflection has attested.
Bitcoin to Ethereum
const b = await tacit.bridgeBurnToPool({
prims, note: { txid, vout, sats, asset, value, blinding }, notePriv,
fee: 0n, // or a ladder fee taken from the minted note
dest: { owner: destOwner },
deriveDestBlinding: (nu) => deriveBridgeMintBlinding({ privkey: priv, nullifier: nu }),
isSpendable: (u) => isPlainSats(u),
});
// once reflection has folded b's block:
await tacit.bridgeMint({ ...b.mintArgs, recovery });- The burn is a standard commit and reveal that any node relays. Derive the destination blinding with
deriveBridgeMintBlinding(bridge-mint-recovery.js) so the minted note comes back from the key. - The mint is relayed and can pay its relay fee from the minted note. At fee 0 it relays within the free daily budget, so a holder with no ETH can finish.
- Check the note first. A note is tracked when its history is inside reflection’s tree;
GET /reflection/note-witness?leaf=finds it. A note whose history reflection never folded bridges by a burn-deposit instead. That takes a provenance bundle registered atPOST /reflection/burndepand a burn that goes straight to a miner because it is non-standard (makeBurnDepositBroadcastersubmits it to MARA Slipstream). A standard burn of an untracked note folds nothing.
mainnet1,000,000 TAC by burn-deposit, burn → mint · a tracked TAC note, burn → mint · a bound tETH note, burn → mint
Ethereum to Bitcoin
const r = await tacit.crossOut({ walletPriv, notes, amount, destOwner, destChain: 1 });
// r carries the claim id read back from CrossOutRecorded (claimIdVerified) and the settle's block
await broadcast.completeCrossOutOnBitcoin({ ...r, ...r.crossOuts[0] }); // dapp/crossout-broadcast.js- Exact amount. The notes must sum to exactly
amount + fee. A bridge burn has no change output.destOwneris the recipient’s x-only Taproot key. - Wait for coverage.
completeCrossOutOnBitcoinbroadcasts only onceGET /reflection/eth-state/coverssays the settle’s block is covered, and only with a verified claim id.tools/build-crossout-mint.mjsbuilds the same pair for review by hand. - Credit.
GET /crossout/minted?asset=&claim=&txid=reports whether a mint was credited.
- The reveal’s output 0 must be a P2TR output paying
destOwner. Anything else is not a member, and nothing reverts. - Build from the claim id the pool emitted, never from a prediction.
- The guest checks a mint once, when it scans it, so a mint broadcast before its block is covered never folds.
mainneta TAC cross-out → mint on Bitcoin, and back to Ethereum by the tracked burn above
The fast lane
Once reflected, a Bitcoin note bound to this pool (T_CXFER_BOUND, 0x39, carrying the pool’s chain binding) can be spent on Ethereum directly, with no burn and mint: tacit.fastlaneExit. The settle proves the note’s membership in the attested Bitcoin tree, its absence from the reflected spent set and a BIP-340 signature by its key. It records the spend, so the note cannot also be spent on Bitcoin. buildTacMigration (burn-deposit-bitcoin.js) moves a reflected note into the bound form in one Bitcoin transaction.
mainnet250,000 TAC spent in the fast lane, then unwrapped to an ERC-20 balance · a tETH fast-lane spend
Notes on Bitcoin
- Split or merge a Bitcoin note with a confidential transfer (
T_CXFER). Each output needs its own P2TR key, which becomes that note’s spend key. - A note that came through reflection nullifies under
btc_note_leaf(asset, cx, cy, authKey), keyed by its own Taproot key. A burn that declares the wrong nullifier never folds, so check it against the note’s live key before broadcasting.
Path 04
cBTC and cUSD
cBTC 0x62a20d98…79c8 · cUSD 0x8f4490dd…679d · minted against self-custody Bitcoin
- Lock. Send BTC to a self-custody output whose transaction carries the lock envelope (
T_CBTC_LOCK,cbtc-lock.js). Pick the funding input first: the cBTC note’s blinding derives from your key and that funding outpoint, which is how recovery finds it. - Escrow. Post a bond of wstETH worth
requiredEscrow(vBtc), 1.5 times the lock today, throughCbtcEscrowHelper:postEscrowWithETH(outpoint)stakes raw ETH (about 320,000 gas),postEscrowWithPermituses a wstETH permit, andreclaimEscrowreturns your share. Anyone may fund a lock. - Wait for reflection to fold the lock;
pool.cbtcLockVBtc(outpoint)then reads its value. - Mint.
tacit.mintCbtc({ walletPriv, outpoint, vBtc, blinding }), relayed and free. The note is a bearer note. A lock mints once; a second mint revertsCbtcLockMismatch.
Borrowing cUSD
Open a position with tacit.defiActions(walletPriv).openCdp. A mint needs 150% collateral, and a position can be liquidated below 130%. cUSD has 8 decimals.
- Rate snapshot. Pass the engine’s live
rate(), which is1e27while the stability fee is dormant. A value outside[RAY, rate()]revertsBadSnapshot. - Save the position secrets first. The position owner key, the debt blinding and the debt note’s
nkexist nowhere else. - Mint at fee 0. A relay fee taken from the debt note leaves you less cUSD than the position owes, so
openCdprefuses one unless you passacknowledgeFeeShortfall: true. - Close by burning debt notes worth at least the debt (an excess is burned, not refunded), with the position’s path from
tacit.cdpPositionTree()and a fresh blinding andnkfor each released leg.
The public ERC-20 forms, tacBTC and tacUSD, come from unwrapping a note, and a mint and an unwrap can share one settle. ConfidentialRouter.zapETHToCanonicalCdpMint buys collateral through zRouter, wraps it and settles your CDP-mint proof in one transaction. CbtcEscrowHelper.postEscrowWithETHAndSettle posts the escrow and settles a self-proved cBTC mint and CDP open together.
mainneta 2,000-sat lock · its escrow · a relayed cBTC mint · CDP open and close
Path 05
Farms · earn TAC
FarmManager 0x000031C47Cb61faB1CE2790a69625FABB71EDE24 · reward wTAC · epoch 1 ends 2026-12-19
The three pools above earn TAC while their LP shares are bonded: TAC/cETH (weight 50), cETH/cUSD (30) and cETH/cBTC (20). Epoch 1 streams 99,700 TAC over 90 days, to unix 1797712559. Rewards are wTAC notes, and each wTAC redeems for one TAC. Read the live program from GET /farm/program?network=mainnet or tacit.farmProgram(), and its checks from GET /farm/health. Weights are governed, so read them instead of hardcoding them.
await tacit.lpBond({ walletPriv, controller: farm.manager, aNote, bNote, feeBps: 30, selfRelay: true });
const positions = await tacit.farmPositions({ walletPriv });
await tacit.farmHarvest({ walletPriv, position: positions[0] }); // harvest first
await tacit.farmUnbond({ walletPriv, position: positions[0] });- One step in.
lpBondadds liquidity and bonds in one settle. It has no fee leg, so send it withselfRelay: true.farmBondbonds an LP note you already hold. - Recovery. The position key derives from your wallet key and the bonded note, so
farmPositionsfinds it again. - Harvest before you unbond. A harvest re-stamps the position, and unbonding forfeits whatever was not harvested.
- Redeeming. Unwrap the wTAC note, then call
WrappedTac.withdraw(amount, to)for TAC.farmRedeemdrives both steps. - Governance. The ops multisig re-weights through a 7-day public queue, at most ±25% per change and one change per 30 days. The rate can only be kept or raised, and the end only moved later. It cannot touch a position.
mainnetone-step add and bond · harvest · unbond · a reward redeemed: unwrap, withdraw
Path 06
TAC ERC-20 · list it in DeFi
0xA1313eb9f3A445606D9583bcAc3ebeB56a858279 · 18 decimals · minted only by the pool
This path is for a DEX, an aggregator, a lending market, a wallet or a market-data site. TAC was etched once on Bitcoin with a supply of 21,000,000 and no mint authority. Its public form on Ethereum is a CanonicalBridgedERC20 that only the Tacit pool can mint, and only against TAC that came from Bitcoin. Verify TAC checks the etch, the token’s code and wiring, its supply and wTAC’s backing in your own browser.
| Read | Returns | Meaning |
|---|---|---|
token.MINTER() | 0x000000000Ed1eabD231Be41d93b719056F7febFC | The pool, the only account that can mint or burn. Immutable. |
token.ASSET_ID() | 0xf0bbe868af10c6c67652a99709bf32048d1aa7194efe3e9a1ef1bde43f94762b | The Bitcoin etch’s asset id. |
pool.canonicalTokenFor(ASSET_ID) | 0xA1313eb9f3A445606D9583bcAc3ebeB56a858279 | The pool names this token as TAC’s public form. |
symbol(), decimals() | TAC, 18 | 8 decimals on Bitcoin, scaled by 1e10. |
name() | Tacit Token | Shared by every token the pool mints, so it identifies nothing. |
contractURI() | ipfs://f0155122039c809872b0d5fff4072c357afe39fe46d1e0edc68e3a578c7e94de138d81231 | EIP-7572 metadata, its hash fixed by the etch. Logo: tacit.finance/brand/logo.svg. |
- The supply is 21,000,000, fixed on Bitcoin.
totalSupply()is only the part public on Ethereum, never more than the etch. It rises when TAC leaves the pool (an exit, or a public swap paid out in TAC) and falls when TAC goes back in (a wrap, or a public swap paid with TAC). On a market-data site, show a maximum supply of 21,000,000. - Amounts. Transfers take any amount; the pool and
TacitPublicAmmtake only whole multiples of1e10. - Nothing non-standard. No transfer fee, rebasing, hooks, pause, blacklist or owner. EIP-2612
permituses the nameTacit Tokenand version1. The canonical Permit2 holds a standing maximum allowance from every holder, so a Permit2 flow needs no approval transaction. - Quote and route. TAC/ETH trades on the Precision pool (0.30%) and on the Tacit pool through
TacitPublicAmmat the 30 bps tier. ETH goes in asmsg.value. TAC going in needs anapprovetoTacitPublicAmm. evm-trade-venues.js has the addresses, quotes and calldata for every venue. - A pool’s spot price is not an oracle. One trade can move either pool. Collateral needs a price that is costly to move, such as a time-weighted average over enough liquidity.
- wTAC is a separate token that escrows farm rewards. List TAC, not wTAC.
- Watch
successor()on the pool. A successor pool would mint its own canonical TAC at another address. This token keeps working and can still be wrapped into this pool and crossed to Bitcoin.
const AMM = '0x00000000E36C7EC997CC59DCda9E03673B448119';
const TAC = '0xf0bbe868af10c6c67652a99709bf32048d1aa7194efe3e9a1ef1bde43f94762b';
const ETH = '0x3cba71e1114af183cdeacc6b8457a474d17529fd28704480ca799d0d03126f34';
const abi = [{ type: 'function', name: 'quoteSwap', stateMutability: 'view',
inputs: [{ type: 'bytes32' }, { type: 'bytes32' }, { type: 'uint32' }, { type: 'uint256' }],
outputs: [{ type: 'uint256' }] }];
// TAC out for 0.01 ETH in
const out = await client.readContract({ address: AMM, abi, functionName: 'quoteSwap', args: [ETH, TAC, 30, 10n ** 16n] });Path 07
TAC airdrop
TacAirdrop 0x4b4cb98D0C836c2783Ac46f0078b904dab533AE8 · 8,652 recipients · claim by 2026-12-20 21:50 UTC
The airdrop is a Merkle claim of public TAC. claim(index, account, amount, proof) can be sent by anyone and always pays account, so a wallet can pay the network fee without being able to redirect the TAC. claimTo and claimAndShield need the eligible account itself.
const s = await tacit.tacAirdrop.status(address); // fetches the proof, checks it against the root, reads the contract
// s.amountTac, s.claimByISO, s.reason: show 'error' as "could not check", never as "not eligible"- Offer
claimandclaimTo. The shield route’s final settle has not been run for this contract, so the client builds it only withallowUnproven: true. - People check and claim at tacit.finance/rewards: look up any address or .eth, .wei or .gwei name, claim from the account itself (to itself or to another address), then farm the TAC.
- Proof files are at
tacit.finance/airdrop/v1/proofs/<xx>.json. tacit.finance sends no CORS header, so a page on another origin reads the commit-pinned mirrors inPUBLIC_PROOF_HOSTS. - The guardian, the ops multisig, can pause claims and sweep the balance. It cannot change the root, the token or the deadline.
Path 08
One key, two chains
a Tacit key from the wallet a user already has, with nothing to back up
Every Tacit key is a secp256k1 key, so one key has both an Ethereum and a Bitcoin address. Tacit apps derive it from one fixed message the user signs: identityMessage({ netName }) in identity-message.js. The key is prfBytesToScalar(sha256(signature)) over the 65-byte signature, with v folded to 27 or 28. An EOA’s signature is deterministic, so reconnecting the same wallet gives the same key. Contract wallets are refused. A Bitcoin wallet can sign the same message instead; the app then signs twice and requires identical output. tacit.finance/kit/tacit-address-kit.js does the derivation and builds the tacit1… address in one dependency-free module.
Anyone who gets it controls all of the user’s Tacit funds. Every Tacit app uses the same text, so a wallet cannot warn about the site. Ask for it only in an app the user trusts. Never send, log or store it, and zero it once the key is derived.
Reference
Relay API
https://api.tacit.finance · public · no key
| Endpoint | What it does |
|---|---|
POST /confidential/submit | { type, op, memos, mode? } → { jobId }. The default mode proves and sends the settle; 'prove' returns the proof for you to send; 'preproven' puts your own proof on the open board. |
GET /confidential/status?id= | pending → proving → settled or failed; a prove job ends at proven with publicValues, proof and the memos settle must carry. A failed with broadcastHashes may still land. |
GET /confidential/relay | Where the relay stands: ok, busy, held, paused or down. |
GET /confidential/open, POST /confidential/landed | The open board of proofs any sender can settle, and closing a job you settled yourself. |
GET /confidential/quote?asset= | The relay fee floor (in-system units) and the recommended tip for a tipped wrap. |
GET /confidential/index | The pool’s event stream and the stealth lock set behind one cursor, for recovery without a scanner. |
GET /reflection/status | The Bitcoin height reflection has attested, the tip and the lag. |
GET /reflection/dump, /reflection/note-witness | The reflected note tree and bridge-burn set, and one note’s path. |
GET /reflection/eth-state/covers?block= | Whether an Ethereum block is covered, before a cross-out mint is broadcast. |
GET /crossout/minted | Whether a cross-out mint was credited. |
GET /farm/program, /farm/health | The farms’ schedule and a solvency verdict, read from the chain. |
Submits are rate-limited per IP. Free settles share a daily budget (429 free_budget) and prove-only jobs share a daily prove budget (429 prove_budget). Past either, attach a relay fee or prove with your own account. While the relay keeps its prover credit for the bridge, a free prove-only job is refused (503 prove_reserved, with a hint naming the way on). Exits are held last, and a relayed settle is not affected. Requests are capped at 1 MiB.
Reference
Proving and fees
every op except wrap() needs an SP1 proof · you choose who proves and who sends
- Hosted, the default. The relay proves on the Succinct network and sends
settle();confidential-relay.jspolls the job for you. - Proof only.
mode: 'prove'returns the proof and you sendsettle()(tacit.submitSettle).selfRelay: trueon an SDK call does both. - Your own prover. Prove with your own Succinct account (
self-prover.js, OPEN-RELAYER.md), and the witness never reaches the relay.
- The relay fee is paid in the op’s own asset, from fee legs inside the op. The relay prices cETH, cUSDC, cUSDT, cUSD, cBTC and cTAC.
- An op with a fee leg must clear the floor from
GET /confidential/quote. Quote withtacit.quoteTransferFeeortacit.quoteOpFee. The program takes only fees on its two-significant-digit ladder. - Ops with no fee relay free within a daily budget: stealth and adaptor locks,
wrap,cbtcmint,farmbond,cdptopupand a zero-feebridgemint. - Pool founding and
lpBondare sent from your own account.
What the relay sees. The witness of the op you hand it: the spent notes’ commitments, owners, paths and nullifier keys, the outputs, the public legs, the authorizing signature or kernel and the sealed memos. Never your wallet key or a note’s blinding. It can prove the op you authorized or decline it; it cannot redirect an output or raise the fee. For a swap it also sees the amounts.
| Group | Type |
|---|---|
| Deposit and send | wrap, transfer, batchtransfer, wraptransfer |
| Exit | unwrap, sendunwrap |
| Trading and liquidity | swap, route, lp, lpremove, lpbond, otc, bid, wrapswap, wraplp |
| Bridge | bridgemint, bridgestealthmint, bridgeburn, fastlane |
| Credit | cdpmint, cdpclose, cdpliquidate, cdptopup, wrapcdpmint, cbtcmint |
| Payments, farms, adaptor | stealthlock, stealthclaim, stealthrefund, farmbond, farmharvest, farmunbond, adaptorlock, adaptorclaim, adaptorrefund |
Reference
Trust and verify
what Tacit hosts is a convenience, not an authority
Nothing Tacit runs decides whether an op is valid. A proof verifies against the pool’s immutable program key or the settle reverts, and reflection data counts only if it reaches the root the pool attested.
| Layer | Hosted | Run it yourself |
|---|---|---|
| Proving and sending | POST /confidential/submit | Your own Succinct account, then settle() from any address |
| Reflection data | /reflection/dump, /reflection/note-witness | Replay the fold over public Bitcoin blocks; a path that misses the attested root is refused anyway |
| Indexes | /confidential/index | Rebuild from the pool’s logs; scanLockLeaves with strict: true checks the result against the pool |
Verify TAC checks TAC’s etch and supply and the ERC-20’s code and wiring from your browser. node tools/verify-live.mjs checks the pool’s code, program keys and roles. You can also check TAC’s fixed supply with nothing installed:
curl -fsSL https://raw.githubusercontent.com/src-company/tacit/main/scripts/verify-tac-supply-standalone.mjs | node --input-type=moduleReference
Addresses and ids
Ethereum mainnet · deploy block 25998736 · source of truth contracts/deployments/1-createx.json
| Contract | Address |
|---|---|
| ConfidentialPool | 0x000000000Ed1eabD231Be41d93b719056F7febFC |
| CollateralEngine | 0x000000003f608BDdF0ca45934003ffb9DbDF70DB |
| ConfidentialRouter | 0x000000005dA3E3B73726af3c774Deeb9472D4992 |
| TacitRelayer | 0x000000009C28617AC88B52Eae5EFaAcdD4aC34c3 |
| TacitPublicAmm | 0x00000000E36C7EC997CC59DCda9E03673B448119 |
| CanonicalAssetFactory | 0x0000000042c2D57499Df64BAF81bfA2C6E100535 |
| BtcCallExecutor | 0x00000000Df8263Ac5810C53B31AaE20ee53C247f |
| EthCallOutbox | 0x00000000a26a6E291972666a9687741dBa11Af46 |
| CbtcEscrowHelper | 0x000000008eCD09f922C9FbbDD9ACA5aE8F0beBfA |
| WrapTipForwarder | 0x000000D218B03db5837943b0b05DeA2965AE956e |
| WrapTokenTipForwarder | 0x0000007b1d93d72f698A861aA86Ac675D6AF7216 |
| SettleTipForwarder | 0x0000008353ee6Dea1236544938C546E27010416D |
| FarmManager | 0x000031C47Cb61faB1CE2790a69625FABB71EDE24 |
| WrappedTac (wTAC) | 0x2018139a8FDd3666855BE3315C7683b4D6aB7AEf |
| TacFarmFunder | 0x7fc40b13c7a99a1d2c41f8b5382978363d18525a |
| TacAirdrop | 0x4b4cb98D0C836c2783Ac46f0078b904dab533AE8 |
| Bitcoin header relay | 0x20A6ddc2C6E620c6248B5A34E85996516FDd19D0 |
| SP1 verifier (Groth16) | 0xb69f2584CBcFf99a58C4e7002E8b89Af54a6f4e2 |
| Ops multisig (engine owner, lineage steward) | 0x006CD14F36F65eCbB29b2519cCBe63A0DC8549F2 |
| zRouter | 0x000000000000FB114709235f1ccBFfb925F600e4 |
| Permit2 | 0x000000000022D473030F116dDEE9F6B43aC78BA3 |
| Program vkey (settle) | 0x006cd47fd23937a6d247696cace28c22d2c6a8280447e6ac45a3571de232d6e3 |
| Bitcoin reflection vkey | 0x00bb158ba04f18a100f998af0e3b074b5368771f22b8b6e4fd1d66823a074bc5 |
| Ethereum reflection vkey | 0x00ca817124b59c05eb6f2731d48a6d7145dc4aff06510e0ba710a7312f6aea72 |
| Asset | Pool asset id | Public ERC-20 |
|---|---|---|
| TAC | 0xf0bbe868af10c6c67652a99709bf32048d1aa7194efe3e9a1ef1bde43f94762b | 0xA1313eb9f3A445606D9583bcAc3ebeB56a858279 |
| ETH (cETH, tETH) | 0x3cba71e1114af183cdeacc6b8457a474d17529fd28704480ca799d0d03126f34 | native ETH |
| cBTC | 0x62a20d98fc1cd20289621d1315294cb8772f934d822e404b71e1f471cf0679c8 | tacBTC 0xdf1d99148bEb7a9AFf1d95C49B3d22b7ed90D696 |
| cUSD | 0x8f4490dd3728b0ee904d7a67c11b37ffd463a5c7f08b79810006995ee8a9679d | tacUSD 0x23cACFFAc2674514A6d4F6cD420B4cc2aC921564 |
| wTAC | 0x1097c9e552ae4fce2a8c416b93403953fa445a5f2cdae8ced36d9a78cfe40832 | 0x2018139a8FDd3666855BE3315C7683b4D6aB7AEf |
| TACD | 0x9cb6a17c0a61a40fa936296e9ec2b428e2a3b7c70cdb76ed25c6937124a3cc11 | 0xA0f6B02561C8ef08335B69aD25e022C0B342147C |
| TACE | 0xda4acdbe7c276dd82c39c3fd470aa1d18d562504b678cfa5a8144c8afddd5db4 | 0x9DE27883C5Dc9Db47BE12E595D59b8f1aBEC8570 |
| Token | Address | What it is |
|---|---|---|
| tacBTC | 0xdf1d99148bEb7a9AFf1d95C49B3d22b7ed90D696 | cBTC in public. |
| tacUSD | 0x23cACFFAc2674514A6d4F6cD420B4cc2aC921564 | cUSD in public. |
| TACD | 0xA0f6B02561C8ef08335B69aD25e022C0B342147C | A dollar borrowed against TAC. |
| TACE | 0x9DE27883C5Dc9Db47BE12E595D59b8f1aBEC8570 | Ether borrowed against TAC. |
Resolve a pool-minted token with canonicalTokenFor(assetId) rather than caching it. The chain binding a burn or mint targets is keccak256(chainId ‖ poolAddress). Private ETH on Ethereum, Base, Robinhood Chain and MegaETH uses a separate, smaller pool, described in its own guide, EVM-POOL.md.
Reference
TAC vaults
borrow TACD or TACE against TAC · source of truth contracts/deployments/tac-vaults.json and dapp/vault/deployments.js
Each vault takes TAC as collateral and lends its own token: the dollar vault lends TACD, the ether vault TACE. The router makes the oracle’s price records, moves TAC in with a permit and borrows on a signature in one transaction; it has no owner or settings. Read a vault’s totals with totalDebt() and debtCeiling(), an account with vaults(owner), and its tokens with TAC() and SYNTH(). The app is tacit.finance/vault.
| Contract | Address |
|---|---|
| Dollar vault (TacVault) | 0x00000087ac9ed3E1Bd443E00FBe18B4EdcAEeb1F |
| Ether vault (TacVault) | 0x00000047bFC8cF504967049D9Cf9E1FD9651ED80 |
| TacVaultRouter | 0x000000F9ab49650502BeC910D13b4717F0082598 |
| TACD (the dollar vault’s token) | 0xA0f6B02561C8ef08335B69aD25e022C0B342147C |
| TACE (the ether vault’s token) | 0x9DE27883C5Dc9Db47BE12E595D59b8f1aBEC8570 |
Reference
Token discovery
token.list.wei · registry 0x0000006013dF75A31678B786061C2B54bf531524
Tacit’s assets are listed on token.list.wei, a general on-chain token registry. A listing with standard = 2 is an ERC-20 on this chain, and its id is its address. A listing with standard = 5 is a Tacit pool asset keyed by asset id, with no balanceOf. Show it apart from plain ERC-20s. Pair a pool asset with its ERC-20 through canonicalTokenFor(assetId). get(id) returns a listing with a ready-to-use logo, and rankedIds() with summariesPaged(start, count) pages the whole registry.
| Asset | Standard | Registry id |
|---|---|---|
| TAC | ERC-20 | its address, 0xA1313eb9f3A445606D9583bcAc3ebeB56a858279 |
| TAC on Bitcoin | Tacit | 0xe64b4fb0dbc77241a053efc73e6e9fa65d5d15c48f4e018aa9d8fb0957086ae8 |
| tacBTC | ERC-20 | its address, 0xdf1d99148bEb7a9AFf1d95C49B3d22b7ed90D696 |
| cBTC | Tacit | 0xa9d7ca4197eb3dc35a62ef673a9c4837230d7fe5221bc7feb5aa7982c1e8ce2a |
| tacUSD | ERC-20 | its address, 0x23cACFFAc2674514A6d4F6cD420B4cc2aC921564 |
| cUSD | Tacit | 0x8dc05a802122ff446cef63c6dbf02a192ae895e645eefd07dedb97af991355a2 |
| tETH | Tacit | 0x948570f2afcf67e3163f741ac52f4e78ec944c4aa56f2a03032910a9a9c35692 |
Reference
When something breaks
a failed proof costs the relay, not you · a settle applies completely or reverts
| Selector | Error | What it means |
|---|---|---|
| 0x9c63840c | AmountNotAligned | The amount is not a multiple of the asset’s unitScale. |
| 0xad2fa98e | DepositExists | This asset, amount and wrap index is already a deposit; take the next index. |
| 0x070d817c | DepositNotPending | The wrap is not mined yet, or its deposit was already consumed. |
| 0x8c520116 | UnknownRoot | The spend root was never a root of this pool: a witness from another pool or an incomplete scan. Rescan. |
| 0xb115d857 | NullifierAlreadySpent | An input note is already spent. |
| 0x2763eb74 | MemoLeafMismatch | The memos do not match the leaves the proof committed to, in count or order. An empty memo is 0x, not 0x00. |
| 0x7fcdd1f4 | ProofInvalid | The proof does not verify against the program key, usually a prover built from the wrong program. |
| 0x5f946d18 | PoolNotInit | A first liquidity add went through settle(); found the pool with createPairAndSettle. |
| 0xafff2f20 | CbtcLockMismatch | The lock does not match, is already minted, spent or redeemed, or its escrow is short. |
| 0x610f890f | BadSnapshot | A CDP mint’s rateSnapshot is outside [RAY, rate()]. |
| 0x8199f5f3 | SlippageExceeded | A public swap or liquidity call paid less than its minimum. |
| 0x55a09618 | ConsumedCountStale | A cross-out or fast-lane spend landed after a reflection proof was built; rebuild it. |
Farm errors: OverClaim (a harvest claimed more than accrued; read pending at build time), NoLivePosition (wrong receipt, or already unbonded) and WrongStakeAsset (not a farmed LP-share asset). A relay refusal at submit means a relay fee below the floor or a full queue, and a 413 means a body over 1 MiB.
Reference
Gotchas
- Output 0 is the cross-out note. A Bitcoin mint reveal pays the declared key as a P2TR output at output 0, or it never folds.
- Wait for coverage. Broadcast a cross-out mint only after
/reflection/eth-state/coverssays yes, built from the claim id the pool emitted. - The nullifier follows the note’s history. A reflected Bitcoin note nullifies under
btc_note_leafwith its own Taproot key. - Four hours of depth. Nothing within 24 blocks of the Bitcoin tip is folded. Say so in the interface, not only in a retry loop.
- Whole notes. A route and an LP removal spend their input whole.
- One key per process. Two processes on one key pick each other’s notes as inputs.
- Send from your own account. Never sign from a key the relay or a keeper uses. Two senders on one nonce sequence stall each other.
- Confirm exits by balance. An
unwrapcan reportsettledfrom the relay’s acknowledgement. The recipient’s balance is the proof.
Reference