Peal Private Links

A payment request is a link. The payer needs a wallet and nothing else. Between deposit and withdrawal, the amount and the two parties are on no chain.

What it is

Peal Private Links is a private payment ledger with a product on top. You create a request (an amount in one asset, a title, an optional reference and expiry), share the link or its QR code, and receive the payment into a private balance. The payer opens the link, connects a wallet, and pays. Deposits and withdrawals are public transactions through a gateway contract; every payment in between is a zero-knowledge proof on a small ledger built for payments.

The ledger is Commonware's Bonsai construction with the ZK-Pari proof system. Each account is one 32-byte commitment. A payment changes two commitments and appends one receipt, proven by a 128-byte proof made in the payer's browser in a few seconds. The ledger checks the proof and learns which account acted. It does not learn the amount, the other party, or whether it was a send or a receive.

Today it runs on Ethereum Sepolia with Circle's testnet USDC and a faucet token, behind a single hosted node. That is a testnet and this is a preview: the section at the end says exactly what you are trusting.

The pieces

Four parts, and the line between them is the privacy boundary. Everything that could identify a payment stays in the box on the left.

your app, with a walletspending key, balance, receiptsproofs are made herenothing that can spend leavesPeal Links noderequests and the directoryencrypted inbox and backupschain watcher, withdrawal signersprivate ledgerone commitment per accounta log of sealed receiptssees who acted, nothing elsegateway contract, Sepoliadeposit(token, amount, tag)withdraw(message, signatures)metadataproof + sealed receiptpublic legsdeposit events
The node is a service. It can delay or refuse, and it keeps metadata, but it never holds a key that opens a balance or a receipt.

How a payment moves

Eight steps. The wallet signs a message in two of them and sends an ordinary transaction in two more; the session with the node is a separate sign-in message, once every twelve hours.

  1. Register. The receiver's wallet signs in, then signs one EIP-712 authorization, PealLinksAccount, that binds their address to a fresh private account and a receiving key (and, for a wallet that signs deterministically, a recovery message that derives the backup key; the app batches these). The node publishes the profile in the directory, so anyone can pay a plain 0x address. The spending key is random and never derived from any of those signatures.
  2. Request. The receiver's private account signs a manifest (amount, title, reference, expiry) and posts it with POST /links/v1/requests. The response is a link, https://peal.network/pay/<id>. No wallet popup.
  3. Open. The payer's client fetches the request and verifies two signatures itself: the manifest against the receiver's account, and the receiver's directory profile against the wallet address in it. It does not take the node's word for either.
  4. Fund, if needed. A deposit is public: the payer proves a deposit relation locally, registers the intent with the node, then calls gateway.deposit(token, amount, tag) from their wallet. After two confirmations the node credits the tag; the wallet claims it with a proof.
  5. Approve. The wallet signs a PealLinksPaymentIntent naming the amount and the recipient's profile hash. That signature is checked in the payer's own client and never transmitted: it is the user saying yes, not a message to a server.
  6. Prove and send. The client proves the payment (about seven seconds in a browser), submits proof and new commitment with POST /links/v1/ledger/{ns}/ops, and drops an encrypted receipt into the receiver's inbox.
  7. Claim. Whenever the receiver is next online, they decrypt the receipt, check its Merkle path against the ledger, and claim it with a proof. Their balance grows. The receipt's position in the log plays the part of a nullifier inside the account's own claimed set; unlike a public nullifier set it is never published, since only the account's owner can produce a valid claim proof for it.
  8. Withdraw. A withdrawal is a send to a fixed burn account, proven like any other, followed by a certificate from the node's settlement signers. Anyone can submit gateway.withdraw(message, signatures); the tokens go to the recipient inside the signed message.

Operation hiding

Every operation publishes a record of the same shape and appends exactly one receipt. A receive appends an unspendable dummy so it looks like a send. An observer of the ledger sees the acting account and a sequence number. That is the entire public record of a private payment.

Who sees what

whoseesdoes not see
the public ledgerwhich account acted, and whenthe amount, the other party, send or receive
the backing chaindeposits and withdrawals: address, amount, tokenwhich private account a deposit went to, any payment in between
the payerthe amount, the receiver's display name and wallet addressthe receiver's balance or other payments
the receiverthe amount and, for a direct send, who paidthe payer's balance or history
the node's directorywhich wallet owns which private accountpayments, amounts, balances
the node's servicesrequest titles and amounts you publish, encrypted receipts, timing, IP addressesreceipt contents, spending keys, balances, payment intents

The node learns the link between a wallet and a private account. It does not make them unlinkable, and the docs never say it does. Correlation by amount and timing across a deposit and a withdrawal remains possible to an observer of the chain.

One wallet is the whole identity

  • Receiving profile. A signed, versioned record in the directory maps a wallet address to a private account and a receiving key. A rename or a revocation is a new version that names the previous one's hash, so a client that has seen version n refuses an older one.
  • Recovery. A wallet that signs deterministically derives a backup key from one signature over a fixed message; the rest get a recovery code shown once. The node keeps an encrypted, versioned backup only that key opens. Losing both the device and the recovery material loses the account, and nobody can help.
  • Several wallets, one device. Each wallet gets its own store. Several tabs of one browser share an account through a lock and a versioned blob, so a stale tab cannot submit against an old commitment.

Using it today

  • Pay and get paid: the hosted app does the whole flow above in the browser, on Sepolia with test funds.
  • Read from your own code: the ledger, a request by id, deposit and withdrawal status, and the node's configuration are plain HTTP reads, no key. "What you can call today" in the API reference lists them.
  • Write from your own code: not public yet. Every write carries a zero-knowledge proof or an account signature, and the client that makes them is not published. A published client is coming.

Two rules any client follows, the hosted app included. Amounts are strings of integer base units on every boundary (12500000 is 12.50 USDC), never floats. And nothing that could spend leaves the client: the node receives proofs, commitments and ciphertext, and the wallet's approval signature is checked locally and thrown away.

What is deployed

  • Ethereum Sepolia (chain 11155111): USDC (Circle's testnet issue) and tUSD (a faucet token), both 6 decimals, two confirmations. Gateway 0xC141Bc6AaED24258276dC203050AD148ec95C1fC. Live behind this site, and the app at https://peal.network/#/bonsai/app.
  • Tempo Moderato (chain 42431): PathUSD and tUSD. Gateway 0xE747A08e7cFea2574bCc9A0a8FCb6E02a68D6F39. Not hosted and not public: the operator runs it on demand.

Everything a client needs to configure itself is in one call: GET https://peal.network/links/v1/status lists the namespaces (chain, token, decimals, gateway, confirmations), the circuit id and the settlement signers. The API reference runs it from the page.

What you are trusting, stated plainly

This is a testnet preview with real cryptography and an unfinished trust model.

Withdrawals are released by a certificate from settlement signers, two of three, whose keys currently live in one process run by the operator. A compromised threshold can release reserves; if the operator disappears, funds not yet withdrawn are stuck. The proving keys come from a per-process setup, not a ceremony. The upstream circuits are a pinned prototype revision with no external review. The ledger is one hosted node, not a validator set. These gaps are about custody and soundness, not a known flaw in the privacy argument; but the circuits and Peal's integration of them are unreviewed, so treat that argument as unaudited too. All of it is why the node refuses to run its settlement signers or the dev-mint fixture beside a mainnet namespace, so no mainnet withdrawal is possible today.

Before real money, all of these are required: a gateway per namespace owned behind a timelock and multisig; a verified token allowlist with no fee-on-transfer or rebasing tokens; settlement signers run by independent operators, keys in HSMs, each checking the ledger before signing; a health check that refuses withdrawals unless the ledger replays cleanly; proving keys from a ceremony; the simulation-extractability question for ZK-Pari resolved; and an external security review. If the operator disappears today, funds inside the ledger cannot be withdrawn; a user with their encrypted backup keeps the openings needed to prove ownership to a successor operator, but nothing automates that. Who sees what is the table above. Do not call the bridge trustless, and do not call the payments unlinkable. They are private, which is a different and true claim.

Where to go next

  • Private Links API: every endpoint under /links/v1, the encodings, the error format and the limits.
  • Use it from an agent: the skill carries a reference file for this, so an agent can integrate it without reading these pages.
  • The product page shows the checkout and the dashboard this is built into.