Skip to content
home/guides/python sdk

zrelay

The native Python client. Same three operations as the TypeScript SDK — shield, attest, track — with an asyncio-first API and blocking wrappers for everything. It talks directly to the public Nitro RPC and a public lightwalletd; there is no Z-Relay server in the path.

Where this belongs
Use the TypeScript SDK in the browser so proving stays on the user's device. Reach for this package when the work is server-side — a payout scheduler, a treasury service, an indexer — and you control the host holding the keys.

Installation

terminal
pip install zrelay

# with the bundled prover wheel (needed for attest())
pip install "zrelay[prover]"

# or with uv
uv add "zrelay[prover]"
Package
zrelay
Python
3.10+
Concurrency
asyncio native; zrelay.sync for blocking
Signing
eth-account
Proving
bundled wheel (manylinux, macOS, Windows)
Typing
PEP 561 inline hints, no stub package

Constructing a client

client.py
1import os2from zrelay import Client3 4client = Client(5    chain_id=4663,                                  # Robinhood Chain mainnet6    private_key=os.environ["PRIVATE_KEY"],7    rpc_url="https://rpc.robinhoodchain.com",       # optional override8    lightwalletd="https://zec.rocks:443",           # optional override9    timeout=180.0,10)

Quoting

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.

quote.py
1from zrelay import QuoteParams, Pool2 3quote = await client.quote(4    QuoteParams(token=usdg, amount=2_500_000_000, pool=Pool.ORCHARD)5)6 7print(quote.estimated_zec)   # "79.12840000"8print(quote.relayer_fee)     # "0.0015"9print(quote.expires_at)      # unix seconds — quotes are short-lived

Shielding

Handles the approval, the DEX route, the escrow deposit and the gas surcharge, then resolves once the Orchard output lands in a finalised block.

shield.py
1import asyncio2from zrelay import Client, ShieldParams, Pool3 4async def main() -> None:5    # One call: swap, escrow, threshold-sign, mint into Orchard.6    settlement = await client.shield(7        ShieldParams(8            token="0xUSDG_ADDRESS",9            amount=2_500_000_000,                   # 2,500 USDG (6 decimals)10            recipient_z_address="u1xyz...orchard_unified_address",11            memo="Quarterly stock-token yield",12            pool=Pool.ORCHARD,13            max_slippage_bps=50,                    # 0.50%14        )15    )16 17    print(settlement.zcash_tx_id)18    print(settlement.status)                        # Status.SETTLED19 20asyncio.run(main())

Attesting

Builds a Groth16 proof about shielded holdings. The viewing key is read from your secret store into process memory and never transmitted — but it is on a machine you operate, which is a materially different risk profile from the browser prover.

attest.py
1from zrelay import AttestParams, MinZecBalance2 3# The witness is built in-process. The viewing key never leaves the host.4attestation = await client.attest(5    AttestParams(6        viewing_key=viewing_key,                    # read from your secret store7        condition=MinZecBalance(8            amount="100.0",9            at_block_height=2_654_900,10        ),11        destination_contract=vault_address,12    )13)14 15print(attestation.claim_hash)16print(len(attestation.proof))                       # ~19217 18# Cheap local check before you pay gas to submit it.19assert await client.verify_attestation(attestation)
Server-side proving moves the risk, it does not remove it
A compromised host holding a full viewing key in memory discloses the whole wallet history. Prefer an incoming viewing key (ivk) where the predicate only concerns received value, scope the secret to a single process, and clear it as soon as the proof exists.

Tracking

Settlement stages arrive as an async iterator, so a worker can record progress instead of polling.

track.py
async for update in client.track(settlement.tracking_id):
    print(update.stage, update.at)
    # Stage.EVM_SEQUENCED | FROST_SIGNED | ORCHARD_INCLUDED | SETTLED

Blocking API

Not every codebase is asyncio-native. zrelay.sync mirrors the full surface with blocking calls and ordinary iterators.

sync.py
1# Every async method has a blocking twin under zrelay.sync, for scripts,2# cron jobs and frameworks that are not asyncio-native.3from zrelay.sync import Client4 5client = Client(chain_id=4663, private_key=os.environ["PRIVATE_KEY"])6settlement = client.shield(params)          # blocks until settled7 8for update in client.track(settlement.tracking_id):9    print(update.stage)

Errors

Every failure subclasses ZRelayError and carries a stable .code. Match on the code or the class, not the message — messages change, codes do not.

errors.py
1from zrelay.errors import (2    ZRelayError,3    QuoteExpired,4    TransparentReceiverOnly,5    InsufficientGasCoverage,6    SlippageExceeded,7    ThresholdNotMet,8)9 10try:11    settlement = await client.shield(params)12except QuoteExpired:13    ...                      # re-quote, do not retry14except TransparentReceiverOnly:15    ...                      # the unified address has no Orchard receiver16except InsufficientGasCoverage:17    ...                      # attach the surcharge18except SlippageExceeded:19    ...                      # the DEX route moved20except ThresholdNotMet as e:21    print(e.tracking_id)     # ring offline; reclaim after the escrow timeout22except ZRelayError as e:23    print(e.code, e.detail)  # every error carries a stable .code

Type checking

typing.py
# The package ships inline type hints (PEP 561) — no stubs to install.
from zrelay import Client, Settlement, Attestation

async def payout(client: Client, amount: int) -> Settlement:
    ...

# mypy and pyright both resolve these without configuration.

Related