u1…) to receive into. Any modern Zcash wallet will generate one.1 — Install
The SDK is the only dependency. ethers is a peer dependency you probably already have; viem works equally well.
npm install @zrelay/sdk ethers2 — Configure
There is no API key. Z-Relay runs no backend, so the SDK talks directly to the public Robinhood Chain RPC and a public lightwalletd endpoint. The only secret in your environment is your own signing key.
# .env.local — every value is public infrastructure
RPC_URL=https://rpc.robinhoodchain.com
LIGHTWALLETD=https://zec.rocks:443
PRIVATE_KEY=0x... # a funded testnet key, never a mainnet one3 — Quote the route
A quote prices the DEX hop, the relayer fee and the expected Orchard output. Quotes are short-lived by design — an expired quote fails loudly rather than settling at a price you did not agree to.
1const quote = await client.quote({2 token: "0xUSDG_ADDRESS",3 amount: ethers.parseUnits("2500", 6),4 pool: "orchard",5});6 7console.log(quote.estimatedZec); // "79.12840000"8console.log(quote.relayerFee); // "0.0015" (0.15%)9console.log(quote.expiresAt); // unix seconds — quotes are short-lived4 — Shield
One call performs the approval, the swap, the escrow deposit and the gas surcharge, then waits for the relayer ring to produce the Orchard output.
1import { ZRelayClient } from "@zrelay/sdk";2import { ethers } from "ethers";3 4const provider = new ethers.JsonRpcProvider("https://rpc.robinhoodchain.com");5const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);6 7const client = new ZRelayClient({8 chainId: 4663, // Robinhood Chain mainnet9 signer,10});11 12// One call: swap, escrow, threshold-sign, mint into Orchard.13const settlement = await client.shield({14 token: "0xUSDG_ADDRESS",15 amount: ethers.parseUnits("2500", 6),16 recipientZAddress: "u1xyz...orchard_unified_address",17 memo: "Quarterly stock-token yield",18});19 20console.log(settlement.zcashTxId); // 9f2a...c41d21console.log(settlement.status); // "SETTLED"When the promise resolves, the value exists as a shielded note. The recipient can spend it from any standard Zcash wallet — Z-Relay is not in the custody path once settlement completes.
5 — Track (optional)
For UI work, track() returns an async iterator over the cross-chain stages so you can render progress instead of a spinner.
1const stream = client.track(settlement.trackingId);2 3for await (const update of stream) {4 console.log(update.stage, update.at);5 // EVM_SEQUENCED → the escrow accepted the deposit6 // FROST_SIGNED → the ring met its threshold7 // ORCHARD_INCLUDED→ the note is in a finalised Zcash block8}Running against testnet
Point the client at chain 46630 and a testnet lightwalletd. Everything else is identical, which means the code you test is the code you ship.
const client = new ZRelayClient({
chainId: 46630, // Robinhood Chain Testnet
signer,
lightwalletd: "https://testnet.lightwalletd.com:9067",
});Common failures
InsufficientGasCoverage— you sent less than the flat surcharge asmsg.value. The SDK attaches it automatically; this usually means a hand-rolled transaction.TransparentReceiverOnly— the unified address you supplied has no Orchard receiver. Settling it would produce a transparent payment, so the SDK refuses rather than silently de-shielding.QuoteExpired— more than the quote window elapsed between quoting and executing. Re-quote; do not retry.- Stuck at
FROST_SIGNED— the ring signed but the Zcash block has not finalised yet. This resolves on its own; the escrow timeout is the backstop.
Where to go next
- SDK reference — every method, option and return type.
- Viewing keys — prove a balance without revealing it.
- Smart contracts — integrate the verifier into your own protocol.