ZYCORD docs
ZycordDocsExchange integration

Exchange integration

What an exchange, custodian or payment processor needs to list ZCD: running a node, deposit addresses, crediting deposits, sending withdrawals, fees and confirmations. Every route named here is described in the RPC API reference.

At a glance#

Mainnet
TickerZCD
Decimals8 (1 ZCD = 100,000,000 drops)
Consensus todayProof of work, RandomX
Target block time30 seconds
Recommended confirmations20 blocks, about 10 minutes
Block reward maturity240 blocks, about 2 hours
Deepest possible reorganization1024 blocks (undo_depth)
Address32 bytes, written as 0x + 64 hex characters
Transaction idCertificate id, 32 bytes, known before signing
Node APIHTTP and JSON on 127.0.0.1:9420, no authentication
Chain id1 (testnet: 2)
Explorerexplorer.zycord.com (testnet: testnet.zycord.com)

Integrate against the public testnet first. It runs the same release under its own chain id, and its coins are free from the faucet.

What is different from Bitcoin#

Most integrations are written against Bitcoin Core's RPC. Four differences change the design of a Zycord integration:

  1. The node has no wallet. It holds no key, so there is no getnewaddress or sendtoaddress. Keys and signing live in zcd, the command-line wallet, which builds a certificate and submits it to the node.
  2. The node keeps no history by address. There is no listtransactions. You read balances, or you walk blocks, and keep your own ledger.
  3. Addresses have no checksum. A mistyped address is still a well-formed address, which nobody owns.
  4. Some addresses are single-use. A one-shot address can be spent from exactly once; after that, a payment sent to it is skipped and the sender pays the skip fee.

Run a node#

Install the release (Installation, Verifying a download), which ships zycordd, the node, and zcd, the wallet. Then:

zycordd --dir /var/lib/zycord           # mainnet
zycordd --testnet --dir /var/lib/zycord-testnet --rpc 127.0.0.1:19420

A build from source names them zycordd-randomx and zcd-randomx.

Before trusting any balance, check that /status answers "syncing": false and that its height matches the explorer. The RPC binds loopback; reach it from your other services over an SSH tunnel or an authenticating proxy (Access), and give those services the trust token so they are not rate-limited.

Run a second node on different infrastructure. zcd cross-checks balances and spent flags against it with --confirm-rpc before a spend, so one node's word is never the only thing sizing a payment that cannot be undone.

Addresses#

An address is 32 bytes: a version byte, then the first 31 bytes of blake3("zcd/addr/v1" ‖ version ‖ Ed25519 public key).

First byteKindA user may withdraw to it
0x01One-shot user addressYes, if /balance answers spent: false and a balance of "0"
0x02Persistent user addressYes
0x05ContractOnly if you support contract deposits
0x00, 0x03Protocol cells, issued assetsNo

Validating a withdrawal address#

# 0x, then 64 hex characters, starting 01 or 02
^0x0[12][0-9a-fA-F]{62}$

Then ask the node: /balance?addr=… must answer spent: false, and for a one-shot (0x01) also a balance of "0". A one-shot that is already spent cannot receive, and paying it costs you the skip fee; zcd refuses to pay a one-shot that is spent or already holds a balance.

There is no checksum

The format catches a wrong length and a wrong character, not a wrong character in the right place. Have users paste or scan addresses rather than type them, and show the address back to them before confirming a withdrawal.

One-shot and persistent#

Wallets give out a fresh one-shot address for every payment by default. The first certificate that spends from a one-shot burns its signing authority, and the reference wallet refuses to pay a one-shot that is spent or that already holds a balance. A one-shot is therefore an address for one deposit.

A persistent address can receive and spend any number of times. It is the right kind for a deposit address a customer keeps; anything paid to it is linked together on chain, which for a deposit address is the point.

Deposits#

Giving each customer an address#

With today's tools, the address a customer can keep is the persistent address of a key file of their own:

zcd wallet new --out customers/1001.key       # prints the first persistent address

Record the address wallet new prints, and give the customer that one. zcd wallet address shows the next unused persistent address, which changes once the first has been paid, so it is not a way to look the deposit address up again.

Each key file is encrypted with its own passphrase and has its own recovery phrase; back both up as you would any hot-wallet key.

For a one-off deposit instead, hand out a one-shot from a single key file:

zcd wallet address --key deposits.key --new

The wallet hands out at most 20 one-shot addresses ahead of the last one that received a payment, so that restoring the key from its recovery phrase still finds every deposit. It learns of payments only when it scans (balance, send, sweep), not on address --new, and every send or sweep from that key file reserves four more one-shot addresses from the same window. A key file with many unpaid addresses outstanding cannot send. That suits a payment page, not an exchange with many customers waiting at once.

Detecting a deposit#

The simplest reliable method is the one the reference wallet uses: read the balance of each deposit address, once a block.

curl -s "http://127.0.0.1:9420/balance?addr=0x02…"

{"balance": "250000000", "spent": false, "tip": {"id": "0x…", "height": 12345}}

Store (address, balance, tip.height, tip.id) each time. An increase is a deposit seen at that height. Read balances with a big-integer type: amounts are decimal strings.

The alternative is to walk the chain: for every new height, fetch /block?height=N&format=ssz, decode its certificates (wire protocol), and take the moves into your addresses from the certificates whose outcome in /block?height=N is APPLIED. This tells you which certificate paid you, but it does not account for every way a balance changes (a matured block reward, for one, credits an address without a move), so reconcile against /balance either way.

Crediting a deposit#

Credit an increase once it has been on the chain for 20 blocks: the tip height is at least 20 past the height you first saw it, and the balance still holds it.

The node answers from its tip only, so "balance at 20 confirmations" is something you keep, not something you ask for. Keep the block id beside each observation: if /block?id=<that id> later answers orphaned: true, a reorganization replaced that block, and the deposits you saw at it must be checked again before they are credited. A deposit already credited and later reversed is the case the 20-block wait exists to make unlikely.

Collecting deposits#

Move a customer's balance to your hot wallet with a sweep:

zcd wallet sweep --key customers/1001.key --to 0x02<hot wallet> \
  --confirm-rpc http://127.0.0.1:9520 --yes

A sweep sends everything except the fee reservation. The unused part of the reservation is refunded in the same block to a new one-shot address of the same key file, and goes out with the next sweep. Spending that one-shot burns it, which asks for a confirmation; --yes answers it, so unattended sweeps do not stop there.

A balance below the skip fee, 0.01 ZCD, cannot pay for its own sweep. Set a minimum deposit above it, or let small deposits wait until later ones join them.

Withdrawals#

zcd wallet send --key hot.key \
  --to 0x02<customer>:150000000 \
  --confirm-rpc http://127.0.0.1:9520 --yes

The amount is in drops. One certificate carries up to 28 --to recipients, fewer when it draws on many of the wallet's addresses, so withdrawals can be batched. Without a terminal, the passphrase is read from the first line of standard input, and --yes skips the interactive confirmation (it skips no check). --dry-run builds and checks the certificate, prints its id and the fee, and submits nothing.

The wallet submits through POST /submit and then follows the certificate for eight blocks, printing what became of it. With --no-wait it returns as soon as the node has taken it, and you follow it yourself:

curl -s "http://127.0.0.1:9420/cert?id=0x…"
AnswerWhat to do
pendingWait
included, outcome: APPLIEDPaid. Record the height and treat it as final after 20 blocks
included, any other outcomeNot paid. A skipped certificate (SKIPPED_STALE, SKIPPED_OVERFLOW) was charged the skip fee. Build a new withdrawal once the skip is 20 blocks deep or the TTL has passed: a reorganization could still carry the same certificate into a block where it applies
droppedThis node removed it from its pool; reason says why. Other nodes may still hold it and include it until its TTL. Rebuild only once the tip is past the TTL and neither of your nodes reports it included
unknownThis node has no record. Ask your second node, and check the blocks since you submitted

Each certificate carries a TTL, 30 blocks by default: a certificate not included by then can never be included, so a withdrawal that missed its window is safe to rebuild. Never rebuild one that may still land; that is how a customer gets paid twice.

Fees#

A certificate's fee is gas times price in two markets, sequential and parallel, each with a base fee the protocol moves by at most an eighth a block, or by one drop per gas when that is more. /fees serves both. The wallet bids a maximum of eight times the current base fee plus a priority tip (100 drops per gas sequential, 5 parallel), reserves that from the balance, and is refunded what was not used in the same block. The base fee is burned and the tip goes to the block's producer.

--dry-run prints the current fee for the certificate it would send, which is the number to show a customer.

A certificate that is included but cannot apply, for example because a one-shot it pays is already spent, is skipped and pays skip_fee, 0.01 ZCD.

Confirmations and reorganizations#

A block is final by depth. The recommended wait is 20 blocks, about 10 minutes. No node reorganizes deeper than 1024 blocks, and checkpoints built into each release pin the chain behind them.

The network moves to proof of stake in era 2, with a block every 5 seconds, and every duration counted in blocks is multiplied by six. The recommended wait stays 10 minutes, which is then 120 blocks, and reward maturity stays about 2 hours. Configure every block-counted wait in time, or update the counts when that era opens.

Block rewards#

A miner's share of a block reward is credited 240 blocks after the block, and /maturing lists the rewards still waiting. Until then it is not in /balance, and a deposit address is never paid by a block reward unless it was used as a payout address.

What the tools do not do yet#

Stated plainly, so you can plan around them:

  • No offline signing. zcd signs and submits from one machine; it does not export an unsigned certificate for a cold signer.
  • One persistent address per key file from the command line.
  • No address history in the node: deposits come from balances or from walking blocks.
  • No address checksum.

An integration that needs more can build and sign certificates itself. The formats are public and fixed: an address is derived as above, and a signature is Ed25519 over blake3("zcd/sig/v1" ‖ chain_id ‖ consensus_root ‖ cert_body_root), where chain_id is 8 bytes little-endian, consensus_root comes from /params, and cert_body_root = blake3("zcd/certbody/v1" ‖ ssz(certificate without signatures)), a different tag from the certificate id's. The encoding is in the wire protocol and docs/PROTOCOL.md, with golden vectors to test against in spec/.

Questions from an integrating team go to the forum thread.