Quickstart
Run a complete ZEC-to-stablecoin swap on a local devnet, then drive the same flow from your own code with the SDK.
This page takes you from a fresh clone to a completed swap on your machine. The local devnet runs a Zcash regtest chain, three EVM chains by default (six with --evm all), both escrow contracts and a maker, so nothing touches a public network and no real funds are involved.
Prerequisites
Install these first. pnpm paralock doctor checks every one of them and prints the same fix commands.
| Tool | Why | Install |
|---|---|---|
| Node.js 20 or newer | Builds and runs the packages and the CLI | From nodejs.org, or nvm install --lts |
| pnpm | The workspace's package manager | corepack enable pnpm, or npm install -g pnpm |
| Docker or OrbStack | Runs the Zcash regtest node; the daemon must be running | brew install orbstack on macOS (or Docker Desktop); Docker Engine on Linux, usable without sudo |
| Foundry 1.8 or newer | anvil runs the local EVM chains (including anvil --network tempo); forge deploys the contracts | curl -L https:/ |
ths | A one-command Zcash regtest (Zakura plus lightwalletd) with a faucet and a dashboard | curl --proto '=https' --tlsv1. |
The commands, ready to copy:
nvm install --lts # or install Node 20+ from nodejs.org
corepack enable pnpm # or: npm install -g pnpm
brew install orbstack # macOS; or Docker Desktop / Docker Engine
curl -L https://foundry.paradigm.xyz | bash && foundryup
curl --proto '=https' --tlsv1.2 -fsSL https://raw.githubusercontent.com/zcashlabs/thus-spoke-zakura/main/install.sh | sh1. Clone and build
git clone https://github.com/bitfalt/Paralock && cd Paralock
pnpm install && pnpm buildpnpm build compiles every package, including the CLI that pnpm paralock runs. Run all the commands below from the repository root.
2. Check your machine
pnpm paralock doctordoctor checks Node, pnpm, the Docker daemon, ths, Foundry (and that its anvil supports --network tempo), that dependencies are installed, packages built and contract libraries present, and that every port is free or already used by the matching Paralock service. Each failed check prints the command that fixes it. Fix them and run doctor again until all required checks pass.
3. Start everything
pnpm paralock dev --webThis one command:
- runs
doctorand stops if a required check fails; - starts Zcash regtest through
ths(a Zakura node plus lightwalletd); - starts one anvil per EVM chain: by default Tempo, Base and Ethereum. Add
--evm allfor all six, which adds Arbitrum, Robinhood Chain and HyperEVM, or pick chains with a comma-separated list such as--evm tempo,arbitrum; - deploys
ParalockHTLC,ParalockAdaptorEscrowand a local stablecoin on each EVM chain (an OUSD stand-in on Tempo, a 6-decimal MockUSD "USDC" elsewhere) and writes every endpoint, address and key to.paralock/devnet.json; - starts the maker in the background, which quotes every devnet chain in both modes and mines a regtest block every 3 seconds;
- with
--web, starts the web app onhttp://localhost:3000(change the port with--web-port).
It reuses anything that is already running, so you can run it again at any time, for example with --evm all to add chains. When it finishes, it prints the endpoints and the next commands to try. Logs of the background processes are in .paralock/maker.log and .paralock/web.log.
What's running
| Service | Address |
|---|---|
| Zcash RPC (Zakura regtest) | http:/ |
| lightwalletd | 127. |
| ths dashboard (wallet balances, faucet) | http:/ |
| Tempo, Base, Ethereum anvils | ports 8545, 8546, 8547 (chain ids 31337, 31338, 31339) |
Arbitrum, Robinhood Chain, HyperEVM anvils (--evm all) | ports 8548, 8549, 8550 (chain ids 31340, 31341, 31342) |
| Maker | http:/ |
Web app (--web) | http:/ |
pnpm paralock devnet status prints the chain heights and contract addresses.
The devnet assigns these roles:
| Role | Zcash | EVM |
|---|---|---|
| Taker | ths account 1 holds the shielded savings; ths account 2 is the swap address Paralock spends from | anvil dev account 1 |
| Maker | ths account 5 | anvil dev account 0 |
| Relayer | none | anvil dev account 2 |
4. Swap ZEC for stablecoins
dev returns once everything is up; the maker and the web app keep running in the background. From the repository root, in the same terminal or a second one:
pnpm paralock swap --zec 1This sells 1 ZEC for OUSD on Tempo in htlc mode. The CLI prints the quote (amount, rate, the 40-block Zcash refund window and the 45-second EVM lock), unshields what the swap needs (the quoted ZEC plus a small fee buffer) from the taker's shielded balance into the swap address, and then prints each step:
| Status | What happened |
|---|---|
zcash_locked | The taker locked the ZEC in a Zcash HTLC |
evm_locked | The maker locked OUSD on Tempo; the taker verified recipient, token, amount and time left |
evm_claimed | The taker claimed the OUSD, which made the secret public |
completed | The maker used the secret to claim the ZEC |
Leftover change goes back into the taker's shielded balance, and the maker shields the ZEC it claimed.
5. Try private mode
pnpm paralock swap --zec 1 --chain base --privateSame swap, settled on Base in USDC, in adaptor mode: the ZEC sits in a plain 2-of-2 multisig, and the secret travels inside adaptor signatures, so no hash appears on either chain. Add --fresh-recipient --relay to receive at a new address created for this swap, with a relayer paying the claim gas. Privacy explains each option.
6. Pay someone an exact amount
pnpm paralock pay --usd 25 --to 0x90F79bf6EB2c4f870365E785982E1f101E93b906 --chain base --label "Acme Coffee"The recipient (here anvil dev account 3) receives exactly 25.00 USDC. The maker quotes exact-out: the dollar amount is fixed and the ZEC amount rounds up. See Payments and payment links.
7. Watch a refund
pnpm paralock swap --zec 0.5 --refund-demoThe taker locks 0.5 ZEC and the maker never responds. Once the chain reaches the refund height, the taker takes the ZEC back and shields it again. No funds are lost.
8. Use the web app
Open http://localhost:3000/app. With the devnet running, the app's API routes run the real SDK against your local chains. It has two flows: Swap (sell an amount of ZEC) and Pay (send exact dollars to an address). A payment link such as http://localhost:3000/app?pay=0x90F79bf6EB2c4f870365E785982E1f101E93b906&usd=25&chain=base&label=Acme%20Coffee opens Pay with everything filled in.
9. Recover a swap
Every swap the CLI or the web app starts is saved in .paralock/taker before anything is broadcast:
pnpm paralock swaps # unfinished swaps
pnpm paralock resume <id> # continue one after a crash or restart
pnpm paralock refund <id> # take the ZEC back once its refund height is reachedPersistence and recovery covers what is stored and how the engines resume.
10. Stop the devnet
pnpm paralock devnet downThis stops the maker and web app that dev started, runs ths stop, stops the anvils Paralock started, and deletes .paralock/devnet.json. Processes Paralock didn't start are left running. The swap stores in .paralock/taker and .paralock/maker are kept.
Step by step instead of dev
dev combines these commands, which you can also run yourself:
pnpm paralock devnet up --evm all # Zcash regtest + EVM anvils + contracts, writes .paralock/devnet.json
pnpm paralock maker # terminal 1: the maker on :7420 (stays in the foreground)
pnpm paralock swap --zec 1 # terminal 2
pnpm --filter web dev # the web app (Next.js allows one dev server per checkout)Use the SDK in your own code
The CLI is a thin layer over @paralock/sdk. The examples/quickstart app is the "hello world": about 60 lines that build a taker with createTaker, get a validated quote, swap, and demonstrate the refund path. Run it against the devnet:
pnpm paralock dev --evm all # from the repo root
pnpm --filter @paralock/example-quickstart start # quote, swap, done (htlc mode)
pnpm --filter @paralock/example-quickstart start -- --private # adaptor mode
pnpm --filter @paralock/example-quickstart start -- --refund # the maker never locks: refundHere is its core. To use it in another project, npm install @paralock/sdk @paralock/zcash and point devnetFilePath at your Paralock checkout.
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { type Swap, createTaker, devnetFilePath } from "@paralock/sdk";
import { ZcashRpc, bytesToHex, randomKeyPair } from "@paralock/zcash";
// Endpoints, contract addresses and the taker's EVM key, read from the devnet as plain config.
// Outside the Paralock checkout, pass its path: devnetFilePath("/path/to/Paralock").
const devnet = JSON.parse(readFileSync(devnetFilePath(), "utf8"));
const base = devnet.evm.base;
// A Zcash key for the swap address. Keep it: htlc refunds are signed with it.
mkdirSync(".quickstart", { recursive: true, mode: 0o700 });
if (!existsSync(".quickstart/zcash.key")) {
writeFileSync(".quickstart/zcash.key", bytesToHex(randomKeyPair().privateKey), { mode: 0o600 });
}
const taker = createTaker({
zcash: { rpcUrl: devnet.zcash.rpc, network: "regtest", key: readFileSync(".quickstart/zcash.key", "utf8") },
evm: {
base: {
rpcUrl: base.rpc,
chainId: base.chainId,
htlc: base.htlc,
adaptorEscrow: base.adaptorEscrow,
account: devnet.taker.evmKey,
fromBlock: base.startBlock,
tokens: [base.tokens[base.quoteToken]], // only accept quotes that pay this stablecoin
},
},
maker: devnet.maker.url,
store: ".quickstart/swaps", // secrets and refund data, written before anything is broadcast
limits: { minClaimWindowSeconds: 15 }, // the devnet's EVM locks last 45 s
mineZcash: (blocks) => new ZcashRpc(devnet.zcash.rpc).generate(blocks), // devnet only
onUpdate: (swap: Swap) => console.log(swap.status.padEnd(14), swap.events.at(-1)?.detail ?? ""),
});
// Fund the swap address. On the devnet the ths faucet pays it; elsewhere you send ZEC from your wallet.
await fetch(`${devnet.zcash.dashboard}/api/v1/faucet/address`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ address: taker.zcashAddress, amount_zatoshi: 10_050_000, idempotency_key: crypto.randomUUID() }),
});
const quote = await taker.quote(10_000_000n, "base", "htlc"); // 0.1 ZEC, validated
const swap = await taker.swap(quote); // lock ZEC, verify the maker's lock, claim
console.log(`${swap.evm.recipient} received ${Number(swap.evm.amount) / 1e6} ${quote.tokenSymbol}`);What each part does:
- Amounts are
bigint. ZEC is in zatoshi (1 ZEC = 100,000,000 zat), so10_000_000nis 0.1 ZEC. Dollars are in token base units with 6 decimals. tokenslists the stablecoins you accept on this chain. The taker rejects any quote that pays a different token.storeis a directory for aFileSwapStore. The swap secret and refund data are written there before the funding transaction is broadcast, so a crash never strands your ZEC.taker.quoteasks the maker for a price and validates it: the maker's key, the chain id (also checked against your RPC node), the token, the lock windows and the expiry.taker.swaplocks the ZEC, waits for the maker's EVM lock, verifies it on-chain and only then claims.mineZcashmines regtest blocks so a refund doesn't wait for real time. Leave it out anywhere else.
The refund path is two calls. lockOnly funds the Zcash lock without involving the maker, and refund returns the ZEC once the refund height is reached. It also works from a fresh process, because everything it needs is in the store.
const quote = await taker.quote(10_000_000n, "base", "htlc");
const locked = await taker.lockOnly(quote); // fund the Zcash lock but never involve the maker
await taker.refund(locked.id); // waits for the refund height, then returns the ZECOn a real network you pass your own node, contracts, keys and maker URL to the same createTaker call; Where Paralock works lists the supported networks and Zcash backends. Contract addresses and start blocks for the public testnets are in contracts/deployments/<chainId>.json and on the contracts page.
Next steps
- Swap on public testnets: the same flow between Zcash testnet and Tempo Moderato, through a public lightwalletd server.
- Where Paralock works: which networks, Zcash nodes and wallets you can use beyond the devnet.
- How it works: what happens at each step and why it is safe.
@paralock/sdkreference: every option and default.- CLI reference: every command and flag.