ZECATHON · track: PRIVATE MARKETS
Live demo: jacksoxx.github.io/shieldbid — the board for the
settled DEMO-01 round: the lot, the sealed state it was in, the receipt list with every txid linked to a
Zcash explorer, and the uniform-price outcome. One static HTML file, no backend, nothing to install — open it
and look around. Walkthrough video (2:42):
jacksoxx.github.io/shieldbid/demo.mp4. Evidence deck:
jacksoxx.github.io/shieldbid/deck/ — seven pages carrying the
same figures, each one linked to the file or the transaction it came from. Repository:
github.com/Jacksoxx/shieldbid
"The price should be public and the participants should not."
ShieldBid runs a sealed-bid auction where the bid is a shielded Zcash transfer — the amount you send is your bid, encrypted end-to-end, readable only by the seller. When the window closes the seller publishes the clearing price and the ranking — nothing else. Nobody on chain ever learns what you bid, who you are, or even how many people took part.
Not another auction site. A missing privacy layer that any auction site can drop in.
This is not a mock-up: the repository ships a complete round that settled on Zcash mainnet, 12 mined transactions, real ZEC, receipts for every leg — see DEMO-01.
On the marketplace this was built against, bids are a public open book — anyone can read every offer and raise the price against you:
| Observation (measured 2026-09-29) | Value |
|---|---|
| Open offers visible on the public bid book (screenshot) | 15 offers · top 3.38 ZEC · 21.81 ZEC total (≈ $33,157) |
| Auction wording on that same page | "existing bids can be viewed or raised at any time before the auction ends" |
| What a participant wrote afterwards | "after analysing other people's bid amounts for 7 hours, I finally submitted my bid" |
Price discovery happens on a public wall. A sealed round puts each bidder's price back inside an encrypted note: unlinkable, unreadable, and settled on the same chain.
- Lot published — item, public reserve price, deadline (UTC), and one unified address (UA) owned by the seller.
- Bid = one shielded → shielded transfer. The transferred amount is the bid. There is no
bid string to parse and no wallet support to depend on; the amount is already encrypted by the
protocol. (
memois accepted and shown in the UI purely as decoration.) - No public book. No amount, no bidder, no bid count is visible to a third party. An explorer
sees
shielded → shieldedand nothing else. - Commitment at the deadline. The seller freezes its inbox at a published block height and
publishes
SHA-256over the sorted set of received notes — before any amount is revealed. - Reveal. The seller publishes the clearing price, the ranking, the count, and the receipt list (txids + amounts + status). Each bidder checks their own txid is in the list.
- Settlement. Winners pay the uniform clearing price (the lowest winning bid); the overpayment comes back, losers are refunded in full, both as shielded payments — so even "this address lost" stays hidden.
Everything is CLI-reproducible: tools/engine.py talks to a local
zkool_graphql light-client engine over loopback; no third-party service is
trusted with keys or amounts.
Round closed 2026-09-28T18:37:46Z at height 3,499,452. 3 sealed bids, 2 slots, reserve 0.003 ZEC. The human bid was sent by hand from a Noir wallet — the two others by the repo's own CLI. Nothing of the user's money is left in any demo wallet.
| # | Bidder | Bid | Result | Pays | Refunded |
|---|---|---|---|---|---|
| 1 | human, from a Noir wallet | 0.012 | win | 0.009 | 0.003 |
| 2 | ROBOT-B (CLI) | 0.009 | win | 0.009 | — |
| 3 | ROBOT-A (CLI) | 0.004 | lose | 0 | 0.004 |
Clearing price 0.009 ZEC = the lowest winning bid (uniform price). Money in 0.032 → out
0.0307, chain fees ≈ 0.001 ZEC (≈ $1.5) for the whole round.
| # | What | ZEC | From → To | txid | Height |
|---|---|---|---|---|---|
| 1 | human's bid for lot 001 | 0.012 | wallet → lot UA | 56bb6efa5a23… |
3,499,413 |
| 2 | funding the demo bidders | 0.020 | user → bidder A | 5c60b3448db4… |
3,499,430 |
| 3 | top-up to bidder B | 0.012 | bidder A → bidder B | a14355bf9522… |
3,499,438 |
| 4 | ROBOT-B sealed bid | 0.009 | bidder B → lot UA | 91fefe434537… |
3,499,451 |
| 5 | ROBOT-A sealed bid | 0.004 | bidder A → lot UA | 4f7926eeadc0… |
3,499,451 |
| 6 | overpayment returned | 0.003 | lot UA → user | 90d35aee9490… |
3,499,452 |
| 7 | loser refunded in full | 0.004 | lot UA → bidder A | f29dbd244048… |
3,499,454 |
| 8 | wind-down sweep | 0.0036 | bidder A → user | 359082d37b80… |
3,499,454 |
| 9 | wind-down sweep | 0.0027 | bidder B → user | feea0b1d60be… |
3,499,454 |
| 10 | wind-down sweep | 0.0039 | bidder A → user | 4441d21eccbc… |
3,499,462 |
| 11 | lot remainder swept | 0.0175 | lot UA → user | 27e1379728d9… |
3,499,462 |
| 12 | memo-carry experiment (finding #1 below) | 0.0005 | lot UA → lot UA | c13e2e4b73ae… |
3,499,418 |
Machine-readable receipts, not hand-typed:
| File | What it is |
|---|---|
docs/demo_01_transactions.json |
every leg read back from the engine's transactionsByAccount |
docs/demo_01_settlement.json |
the reveal: clearing price, ranking, commit/salt per bid |
docs/demo_01_money_trail.json |
the money path in / out |
docs/demo_01_seller_view.json |
what the seller's own wallet decrypted |
docs/demo_01_ledger.json |
bidder receipts + commitments |
docs/DEMO_01_REPORT.md |
the full write-up, including the honest limits |
- The memo cannot carry a bid — so the amount carries it. A received orchard note reads back
memo = nullandmemosByTransactionreturns[]on engine v6.31.0: the node does not hand the memo to the wallet. The protocol was redesigned around escrow-as-bid (the amount is the bid), which is strictly better — no memo convention to agree on, no wallet support needed, and the amount is ciphertext by construction. - A fresh outbound payment needs a fresh account sync. Paying twice in a row from one wallet
without
synchronizeAccountin between makes the node reject the tx (could not contextually validate— the wallet was still offering a note it had just spent).demo_01_run.pay()now syncs before every attempt, so retries self-heal.
bash tools/engine_up.sh # start the local light-client engine (mainnet + testnet)
# 1) seller: show the lot address that bidders pay
python tools/engine.py addresses --net mainnet --account 1
# 2) bidder: place a sealed bid (amount = your price)
python tools/engine.py bid --net mainnet --account 2 --to <lot-ua> --amount 0.01
# 3) seller: sync, then read the sealed bids / close the round
python tools/engine.py sync --net mainnet --account 1 --balance
python tools/engine.py bids --net mainnet --account 1 --jsonA bidder with any shielded Zcash wallet (Zodl, Ywallet, Edge, Zkool, Zingo!, Nighthawk, Noir) needs no tooling at all:
send the amount to the lot address. See docs/BIDDER_GUIDE.md.
Engine setup, GraphQL cheat-sheet and the pitfalls that cost real time are in
docs/ENGINE_SETUP.md.
web/index.html the public lot board (single file, no backend)
docs/deck.html the evidence deck (built from the recording canvas; every figure is a link)
docs/fonts/ the deck's pixel fonts — SIL OFL, licence text shipped alongside
docs/SPEC.md protocol spec SHIELDBID/1, incl. the threat model
docs/BIDDER_GUIDE.md wallet-level instructions for bidders
docs/DEMO_01_REPORT.md the mainnet round: receipts + honest limits
docs/ENGINE_SETUP.md local zkool_graphql engine: setup, GraphQL, pitfalls
tools/engine.py wallet CLI: height/accounts/addresses/balance/sync/notes/bids/bid/send/newaccount/shield/validate
tools/demo_01_run.py drives a full round: status/fund/bid/close/refund
tools/demo_01_finish.py wind-down: close → refund → sweep (idempotent)
tools/*_card.py the PNG result/status/roadmap cards used in this README
tools/watch_bids.py poll the lot UA for newly received notes
tools/publish_site.sh rebuild the live demo: console + video + deck → the gh-pages branch
tools/build_deck.py rebuild docs/deck.html from spectmp/deck.html (the recording canvas)
Honest limits (stated, not hidden)
- No live "current high bid" exists. Amounts are encrypted, so the seller cannot honestly publish a standing price during the round. We do not fake one.
- The seller is a trusted party. It can decrypt from the first block, and it can lie about the
arithmetic behind the published clearing price. Third parties cannot recompute it from chain
data alone — that is the price of privacy. What a bidder can do is prove what it bid: every bid
carries a commit/salt record (
docs/demo_01_ledger.json), and every revealed bid must be backed by a real txid paying the lot address. - A sent shielded transfer is final. A bidder cannot retract a bid after the deadline, so a bid must be a price it is happy to pay.
- Custody is the seller's until refunds are broadcast. Mitigation: the seller publishes the refund batch txids.
- The asset itself is out of scope. Zcash has no live mainnet asset/NFT standard, so the lot's item is referenced off-chain by id/URI. ShieldBid settles the price discovery.
- Escrow-as-bid means the bid amount is visible to whoever holds the lot keys. That is inherent
to the design and stated in
docs/SPEC.md§7.
An independent ZECATHON submission. Not affiliated with, and not repackaging, any existing product; the public bid book referenced above is cited only as evidence that the problem is real.
MIT licensed — see LICENSE.

