tacitbuild

tacit build

Everything to build on Tacit V1: the Tacit pool on Ethereum, its bridge to Bitcoin, cBTC and cUSD, the TAC vaults, farms and the TAC ERC-20. The client is plain ES modules with no build step. The API is public, with no keys to request. Each path lists the rules that matter and the mainnet transactions behind it.

Confidential pool0x000000000Ed1eabD231Be41d93b719056F7febFC
Relay and reflection APIhttps://api.tacit.finance
Sourcegithub.com/src-company/tacit

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.
Every op, its SDK call and its relay type
OpSDK callRelay typeHow it landsPath
Deposit ETH or a tokenwrap, then submitWrapSettlewrapThe deposit is your own transaction; its settle is relayed free01
Deposit and split at oncewrapAndSendwraptransferThe relay proves; you send the router transaction01
Send to yourself, split, mergetransfertransferRelayed, fee at or above the floor01
Pay someone elsestealthSend, stealthClaim, stealthRefundstealthlock, stealthclaim, stealthrefundRelayed01
Exit to an addressunwrap, sendUnwrapunwrap, sendunwrapRelayed, fee at or above the floor01
Exit to Base or Robinhood Chainexit builders in confidential-router.jsunwrap, then activateExitA relayed unwrap and a permissionless activation, or one exitAndExecute01
Swap or routeroute, quoteRouterouteRelayed02
Add or remove liquiditylpAdd, lpRemovelp, lpremoveRelayed; founding a pool is self-submitted02
Bitcoin to EthereumbridgeBurnToPool, then bridgeMintbridgemintA Bitcoin burn, then a relayed mint (free at fee 0)03
Ethereum to BitcoincrossOut, then completeCrossOutOnBitcoinbridgeburnA relayed burn, then a Bitcoin commit and reveal03
Spend a Bitcoin-homed note on EthereumfastlaneExitfastlaneSelf-submitted by default03
Lock BTC, mint cBTCcbtc-lock.js, CbtcEscrowHelper, mintCbtccbtcmintRelayed, free04
Borrow and repay cUSDdefiActions(walletPriv).openCdp, closeCdp, topupCdpcdpmint, cdpclose, cdptopupRelayed04
FarmlpBond, farmBond, farmHarvest, farmUnbond, farmRedeemlpbond, farmbond, farmharvest, farmunbondRelayed; lpBond is self-submitted05

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 commitment C = value·H + blinding·G is 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 immutable PROGRAM_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 / unitScale and must divide exactly; ETH, TAC, cBTC and cUSD use 1e10.
  • 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.wrap takes the next unused index. If the settle step fails, buildWrap with the same index rebuilds the same deposit. The escrow has no deadline, and wrapping again would deposit twice.
  • One transaction. wrapAndSend wraps and settles together and splits the deposit into an amount and change. To pay the relay inside a plain deposit, send it through WrapTipForwarder with the tip GET /confidential/quote recommends.
  • Paying someone else. stealthSend locks to a one-time key derived from their tacit1… address, and they claim it. The sender can refund after the lock’s deadline, about 90 days by default. A .wei, .gwei or .eth name can carry that address in its finance.tacit text record; confidential-names.js resolves and publishes it.
  • Exiting. unwrap pays a whole note out; sendUnwrap pays 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 no withdraw() and no receive(). An exit is the unwrap op, a proof that settle applies. 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.

Two rules for the L2 exit
  • Arbitrum aliases contract callers: use createRetryableTicket naming the recipient, and check eth_getCode on any refund address.
  • exitAndExecute needs about 2,000,000 gas and activateExit 800,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 lpRemove burns a whole share note. Split with transfer first to use part of one.
  • Founding. The first add goes through createPairAndSettle from your own account (lpAdd with selfRelay: true). A relayed first add reverts PoolNotInit. The founding amounts set the price, and 1,000 shares stay locked for good.
  • In ratio. After founding, add the amount quoteLpAdd returns. 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.

Pools with standing liquidity · 30 bps · the farm pools
PairPool idLP-share asset id
TAC / cETH0x248497bf6f943cd2b39a04bf5841056c58dfd7ef196188cb4f0ac1fd11dc7c000x17c56713a7e4a5d679a71def3ff9fa186f1556ef757b0ee6b7a3ed8c9249ef99
cETH / cUSD0x5925c0c2954c5b11bedd20e444f0cb22f3827f6197814f97d193e7a44e909da70xd608b0c3806e782cc213e2d52245c3c2fbef455a10410a1f5ccc61ba45262571
cETH / cBTC0x8359cd1f812e3a9040129e186e1477f60f2188bad1bd6a56e5506b22bf5cd3310x0a0cce175bc483945822c8de3d3926e813269f5e1bbe1f9c853e09ed48b68254

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

Bitcoin: bridge burn (0x2B)→reflection folds it→settle: OP_BRIDGE_MINT→note on 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 at POST /reflection/burndep and a burn that goes straight to a miner because it is non-standard (makeBurnDepositBroadcaster submits 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

settle: OP_BRIDGE_BURN→Bitcoin: T_CROSSOUT_MINT (0x65)→reflection folds it→note on 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. destOwner is the recipient’s x-only Taproot key.
  • Wait for coverage. completeCrossOutOnBitcoin broadcasts only once GET /reflection/eth-state/covers says the settle’s block is covered, and only with a verified claim id. tools/build-crossout-mint.mjs builds the same pair for review by hand.
  • Credit. GET /crossout/minted?asset=&claim=&txid= reports whether a mint was credited.
A wrong cross-out mint fails silently
  • 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

  1. 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.
  2. Escrow. Post a bond of wstETH worth requiredEscrow(vBtc), 1.5 times the lock today, through CbtcEscrowHelper: postEscrowWithETH(outpoint) stakes raw ETH (about 320,000 gas), postEscrowWithPermit uses a wstETH permit, and reclaimEscrow returns your share. Anyone may fund a lock.
  3. Wait for reflection to fold the lock; pool.cbtcLockVBtc(outpoint) then reads its value.
  4. Mint. tacit.mintCbtc({ walletPriv, outpoint, vBtc, blinding }), relayed and free. The note is a bearer note. A lock mints once; a second mint reverts CbtcLockMismatch.

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 is 1e27 while the stability fee is dormant. A value outside [RAY, rate()] reverts BadSnapshot.
  • Save the position secrets first. The position owner key, the debt blinding and the debt note’s nk exist nowhere else.
  • Mint at fee 0. A relay fee taken from the debt note leaves you less cUSD than the position owes, so openCdp refuses one unless you pass acknowledgeFeeShortfall: 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 and nk for 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. lpBond adds liquidity and bonds in one settle. It has no fee leg, so send it with selfRelay: true. farmBond bonds an LP note you already hold.
  • Recovery. The position key derives from your wallet key and the bonded note, so farmPositions finds 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. farmRedeem drives 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.

Identify it by these reads, never by name
ReadReturnsMeaning
token.MINTER()0x000000000Ed1eabD231Be41d93b719056F7febFCThe pool, the only account that can mint or burn. Immutable.
token.ASSET_ID()0xf0bbe868af10c6c67652a99709bf32048d1aa7194efe3e9a1ef1bde43f94762bThe Bitcoin etch’s asset id.
pool.canonicalTokenFor(ASSET_ID)0xA1313eb9f3A445606D9583bcAc3ebeB56a858279The pool names this token as TAC’s public form.
symbol(), decimals()TAC, 188 decimals on Bitcoin, scaled by 1e10.
name()Tacit TokenShared by every token the pool mints, so it identifies nothing.
contractURI()ipfs://f0155122039c809872b0d5fff4072c357afe39fe46d1e0edc68e3a578c7e94de138d81231EIP-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 TacitPublicAmm take only whole multiples of 1e10.
  • Nothing non-standard. No transfer fee, rebasing, hooks, pause, blacklist or owner. EIP-2612 permit uses the name Tacit Token and version 1. 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 TacitPublicAmm at the 30 bps tier. ETH goes in as msg.value. TAC going in needs an approve to TacitPublicAmm. 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 claim and claimTo. The shield route’s final settle has not been run for this contract, so the client builds it only with allowUnproven: 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 in PUBLIC_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.

The signature is the key

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

EndpointWhat 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/relayWhere the relay stands: ok, busy, held, paused or down.
GET /confidential/open, POST /confidential/landedThe 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/indexThe pool’s event stream and the stealth lock set behind one cursor, for recovery without a scanner.
GET /reflection/statusThe Bitcoin height reflection has attested, the tip and the lag.
GET /reflection/dump, /reflection/note-witnessThe 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/mintedWhether a cross-out mint was credited.
GET /farm/program, /farm/healthThe 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

  1. Hosted, the default. The relay proves on the Succinct network and sends settle(); confidential-relay.js polls the job for you.
  2. Proof only. mode: 'prove' returns the proof and you send settle() (tacit.submitSettle). selfRelay: true on an SDK call does both.
  3. Your own prover. Prove with your own Succinct account (self-prover.js, OPEN-RELAYER.md), and the witness never reaches the relay.
Fees
  • 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 with tacit.quoteTransferFee or tacit.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, cdptopup and a zero-fee bridgemint.
  • Pool founding and lpBond are 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.

Relay types
GroupType
Deposit and sendwrap, transfer, batchtransfer, wraptransfer
Exitunwrap, sendunwrap
Trading and liquidityswap, route, lp, lpremove, lpbond, otc, bid, wrapswap, wraplp
Bridgebridgemint, bridgestealthmint, bridgeburn, fastlane
Creditcdpmint, cdpclose, cdpliquidate, cdptopup, wrapcdpmint, cbtcmint
Payments, farms, adaptorstealthlock, 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.

LayerHostedRun it yourself
Proving and sendingPOST /confidential/submitYour own Succinct account, then settle() from any address
Reflection data/reflection/dump, /reflection/note-witnessReplay the fold over public Bitcoin blocks; a path that misses the attested root is refused anyway
Indexes/confidential/indexRebuild 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=module

Reference

Addresses and ids

Ethereum mainnet · deploy block 25998736 · source of truth contracts/deployments/1-createx.json

ContractAddress
ConfidentialPool0x000000000Ed1eabD231Be41d93b719056F7febFC
CollateralEngine0x000000003f608BDdF0ca45934003ffb9DbDF70DB
ConfidentialRouter0x000000005dA3E3B73726af3c774Deeb9472D4992
TacitRelayer0x000000009C28617AC88B52Eae5EFaAcdD4aC34c3
TacitPublicAmm0x00000000E36C7EC997CC59DCda9E03673B448119
CanonicalAssetFactory0x0000000042c2D57499Df64BAF81bfA2C6E100535
BtcCallExecutor0x00000000Df8263Ac5810C53B31AaE20ee53C247f
EthCallOutbox0x00000000a26a6E291972666a9687741dBa11Af46
CbtcEscrowHelper0x000000008eCD09f922C9FbbDD9ACA5aE8F0beBfA
WrapTipForwarder0x000000D218B03db5837943b0b05DeA2965AE956e
WrapTokenTipForwarder0x0000007b1d93d72f698A861aA86Ac675D6AF7216
SettleTipForwarder0x0000008353ee6Dea1236544938C546E27010416D
FarmManager0x000031C47Cb61faB1CE2790a69625FABB71EDE24
WrappedTac (wTAC)0x2018139a8FDd3666855BE3315C7683b4D6aB7AEf
TacFarmFunder0x7fc40b13c7a99a1d2c41f8b5382978363d18525a
TacAirdrop0x4b4cb98D0C836c2783Ac46f0078b904dab533AE8
Bitcoin header relay0x20A6ddc2C6E620c6248B5A34E85996516FDd19D0
SP1 verifier (Groth16)0xb69f2584CBcFf99a58C4e7002E8b89Af54a6f4e2
Ops multisig (engine owner, lineage steward)0x006CD14F36F65eCbB29b2519cCBe63A0DC8549F2
zRouter0x000000000000FB114709235f1ccBFfb925F600e4
Permit20x000000000022D473030F116dDEE9F6B43aC78BA3
Program vkey (settle)0x006cd47fd23937a6d247696cace28c22d2c6a8280447e6ac45a3571de232d6e3
Bitcoin reflection vkey0x00bb158ba04f18a100f998af0e3b074b5368771f22b8b6e4fd1d66823a074bc5
Ethereum reflection vkey0x00ca817124b59c05eb6f2731d48a6d7145dc4aff06510e0ba710a7312f6aea72
AssetPool asset idPublic ERC-20
TAC0xf0bbe868af10c6c67652a99709bf32048d1aa7194efe3e9a1ef1bde43f94762b0xA1313eb9f3A445606D9583bcAc3ebeB56a858279
ETH (cETH, tETH)0x3cba71e1114af183cdeacc6b8457a474d17529fd28704480ca799d0d03126f34native ETH
cBTC0x62a20d98fc1cd20289621d1315294cb8772f934d822e404b71e1f471cf0679c8tacBTC 0xdf1d99148bEb7a9AFf1d95C49B3d22b7ed90D696
cUSD0x8f4490dd3728b0ee904d7a67c11b37ffd463a5c7f08b79810006995ee8a9679dtacUSD 0x23cACFFAc2674514A6d4F6cD420B4cc2aC921564
wTAC0x1097c9e552ae4fce2a8c416b93403953fa445a5f2cdae8ced36d9a78cfe408320x2018139a8FDd3666855BE3315C7683b4D6aB7AEf
TACD0x9cb6a17c0a61a40fa936296e9ec2b428e2a3b7c70cdb76ed25c6937124a3cc110xA0f6B02561C8ef08335B69aD25e022C0B342147C
TACE0xda4acdbe7c276dd82c39c3fd470aa1d18d562504b678cfa5a8144c8afddd5db40x9DE27883C5Dc9Db47BE12E595D59b8f1aBEC8570
The public ERC-20s and what each one is
TokenAddressWhat it is
tacBTC0xdf1d99148bEb7a9AFf1d95C49B3d22b7ed90D696cBTC in public.
tacUSD0x23cACFFAc2674514A6d4F6cD420B4cc2aC921564cUSD in public.
TACD0xA0f6B02561C8ef08335B69aD25e022C0B342147CA dollar borrowed against TAC.
TACE0x9DE27883C5Dc9Db47BE12E595D59b8f1aBEC8570Ether 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.

ContractAddress
Dollar vault (TacVault)0x00000087ac9ed3E1Bd443E00FBe18B4EdcAEeb1F
Ether vault (TacVault)0x00000047bFC8cF504967049D9Cf9E1FD9651ED80
TacVaultRouter0x000000F9ab49650502BeC910D13b4717F0082598
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.

AssetStandardRegistry id
TACERC-20its address, 0xA1313eb9f3A445606D9583bcAc3ebeB56a858279
TAC on BitcoinTacit0xe64b4fb0dbc77241a053efc73e6e9fa65d5d15c48f4e018aa9d8fb0957086ae8
tacBTCERC-20its address, 0xdf1d99148bEb7a9AFf1d95C49B3d22b7ed90D696
cBTCTacit0xa9d7ca4197eb3dc35a62ef673a9c4837230d7fe5221bc7feb5aa7982c1e8ce2a
tacUSDERC-20its address, 0x23cACFFAc2674514A6d4F6cD420B4cc2aC921564
cUSDTacit0x8dc05a802122ff446cef63c6dbf02a192ae895e645eefd07dedb97af991355a2
tETHTacit0x948570f2afcf67e3163f741ac52f4e78ec944c4aa56f2a03032910a9a9c35692

Reference

When something breaks

a failed proof costs the relay, not you · a settle applies completely or reverts

SelectorErrorWhat it means
0x9c63840cAmountNotAlignedThe amount is not a multiple of the asset’s unitScale.
0xad2fa98eDepositExistsThis asset, amount and wrap index is already a deposit; take the next index.
0x070d817cDepositNotPendingThe wrap is not mined yet, or its deposit was already consumed.
0x8c520116UnknownRootThe spend root was never a root of this pool: a witness from another pool or an incomplete scan. Rescan.
0xb115d857NullifierAlreadySpentAn input note is already spent.
0x2763eb74MemoLeafMismatchThe memos do not match the leaves the proof committed to, in count or order. An empty memo is 0x, not 0x00.
0x7fcdd1f4ProofInvalidThe proof does not verify against the program key, usually a prover built from the wrong program.
0x5f946d18PoolNotInitA first liquidity add went through settle(); found the pool with createPairAndSettle.
0xafff2f20CbtcLockMismatchThe lock does not match, is already minted, spent or redeemed, or its escrow is short.
0x610f890fBadSnapshotA CDP mint’s rateSnapshot is outside [RAY, rate()].
0x8199f5f3SlippageExceededA public swap or liquidity call paid less than its minimum.
0x55a09618ConsumedCountStaleA 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/covers says yes, built from the claim id the pool emitted.
  • The nullifier follows the note’s history. A reflected Bitcoin note nullifies under btc_note_leaf with 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 unwrap can report settled from the relay’s acknowledgement. The recipient’s balance is the proof.

Reference

Further reading