Notes
Shorter than a post, longer than a tweet. TILs, links worth commenting on, and the occasional small rant. Notes-only RSS →
-
Egress is the new vendor lock-in
Two of the big three quietly raised egress this quarter while announcing “AI-friendly” pricing on inbound. Storage is cheap, compute is cheap, the bill is the door. Pick your stack assuming you’ll have to evacuate it under load. Anyone telling you “the data lives where the GPUs live” is selling you a one-way ticket.
-
Live crowd counter HUD
Threw a live-crowd HUD on the pike_pupday site. Polls a
/api/countevery 10s, animates the integer with framer-motion. Konami code on the page swaps in a different overlay (yes, it’s a leather-bar event site — what did you expect).The thing I’m proudest of is the not-dead-when-API-is-down fallback. If the API hasn’t returned a number in 60s, the HUD fades to grey and shows ”—” instead of pretending the last cached count is current. Loud failure, not silent staleness.
-
compress / decompress helpers for Light Protocol tokens
Added
CompressedTokenHelper.compress(...)and.decompress(...)to the z_trade SDK. Light Protocol’s ZK Compression charges roughly 0.002 for a regular SPL token account — a 500× cost reduction once you’re moving more than a few hundred accounts.The helper does the boring CPI plumbing: wrap-and-decompose the user’s instruction, route through the Light System Program, attach the 128-byte Groth16 validity proof from a Photon RPC. Saved every consumer ~80 lines of boilerplate.
-
Stuck sell path on graduated launchpad tokens
A user reported they couldn’t sell tokens that had graduated from the launchpad to the open AMM. Swap quote returned valid, swap submit returned
0x1771— slippage exceeded. Except slippage tolerance was 50%.Real cause: the bonding curve’s
migrate_authorityhad handed liquidity to the AMM, but our frontend was still pulling quotes from the bonding-curve route. Fixed by checking the migration flag on the mint account before quote routing. Defensive code I should have written when the launchpad shipped. -
Darwin frameworks bundled, wallet-ui types pinned
Shipped the macOS sidecar bundling pass for vanta-desktop. The
setup-sidecars.shscript now copies the right Rust frameworks intoFrameworks/so notarization stops failing on missing dylibs. v2 test renames sovanta_v2_*.rsreads consistently with thevanta-node-v2rust crate name.The thing nobody tells you about Tauri 2.x sidecars: the binary name in
tauri.conf.jsonmust match the target triple-suffixed binary on disk.bitcoind-aarch64-apple-darwinis what the loader looks for;bitcoindalone gets ignored. -
BTC RPC tunnel scripts for swap testing
Shipped the btc-tunnel.sh family for vanta-desktop. Two parts: an SSH local-forward to a remote
bitcoind’s RPC port, and an address-watcher loop that polls for confirmations on a set of P2WPKH addresses without exposing your local node.Pattern that’s saved me twice now: never run
bitcoindon0.0.0.0:8332. Always bind to127.0.0.1:8332and SSH-forward. The number of testnet bitcoinds I’ve seen with public RPC binding is non-zero and every one of them has been mined for spam. -
Private atomic-swap price discovery, draft 1
Spent yesterday writing the price-discovery design for vanta-swap. The interesting part isn’t the HTLC primitive — that’s been solved since BIP-199 — it’s the price oracle problem when both legs are private.
If neither side reveals the amount, who arbitrates the rate? Three options: pre-committed price ranges (boring), threshold-decrypter set (works but adds trust), or a verifiable shuffle over a batch of bids. Going with option 3.
-
ZK transfers as first-class citizens in vanta-explorer
The explorer used to render ZK transfers as “unknown opcode” because the parser was Bitcoin-Core-flavoured and didn’t speak our witness-v2. Refactored to detect the privacy opcode prefix early in the decoder, then route to a dedicated renderer that shows nullifiers, commitments, and proof byte length without ever trying to interpret the encrypted note ciphertext.
Genesis scan was the surprise win: by walking from height 0 with the new parser, I found 4 mis-attributed legacy txns that the old explorer had been showing as “coinbase” forever. Bug fix and historical-correctness fix in one commit.
-
vanta-explorer v2: API + SPA on a single port
The explorer was running as two separate Fly machines: one Node API at
:3000, one nginx-served SPA at:80. Doubled the rent, doubled the surface area. Consolidated into a single Express handler that serves the SPA on/and the JSON API on/api/*.The thing nobody tells you: Fly’s free tier is metered per machine, not per port. A 2-machine setup of two 256MB VMs is twice the cost of one 512MB VM serving both. Saved $11/month.
-
Remote /prove endpoint + desktop Linux WebKit fix
For users on phones or low-end laptops, generating a Groth16 proof in 1.5s is still 1.5s of dead UI. Added a
/proveendpoint on vanta-explorer that takes a witness, runs the prover server-side, and returns the 128-byte proof. Trust trade-off documented in the desktop app — server sees the witness, but only the user sees the resulting note keys.Same commit fixes the Linux WebKit build for vanta-desktop. Tauri’s WebView2 default works on Windows and macOS but Linux needs
WEBKIT_DISABLE_COMPOSITING_MODE=1for the proof-modal animation to not crash on AMD GPUs. Three hours of bisecting to find that one line. -
COINBASE_MATURITY=30 + L2 /submit stops mutating SMT
Bitcoin uses
COINBASE_MATURITY=100(you can’t spend a coinbase output until 100 blocks deep). With our 1-minute target block time, that’s 100 minutes of waiting before mining rewards become spendable. Cut to30for a smoother dev/UX experience. Still safe — the reorg history on Vanta is dominated by 1-deep reorgs.Bigger fix in the same commit: the L2
/submitendpoint was mutating the sparse Merkle tree even on validation failure, leaving us with phantom commitments that no proof could ever spend. Now it builds the candidate state, runs every check, and only mutates aftervalidate_v2()returns true. -
Devnet up, fixed shielded-pool program ID
Stood up the SDK devnet docs + quickstart guide. The shielded-pool program ID had a stale value in the SDK’s default config — three minutes of “why does my proof verify but the CPI fails” before I noticed. Hardcoded constant in
crates/zera-sdk-rs/src/constants.rswas pointing at the old program; now derived fromZERA_NETWORKenv so devnet/mainnet stay separate by construction.Tip if you’re ever doing this: move the program ID derivation into the same module as the IDL bindings. If they’re in different files, future-you will pin them at different times and lose 3 minutes per drift event.
-
ZK activation at height 997 (early blocks have no commitment)
Found a fun chain-init bug. The ZK proof verification path was being triggered on every block from genesis, but the first ~1000 blocks were minted before the shielded-pool program was deployed. The verifier kept trying to read a commitment tree that didn’t exist; transactions silently dropped.
Fix: hardcode
ZK_ACTIVATION_HEIGHT = 997so blocks 0..996 skip ZK verification entirely (legacy-validation-only) and 997+ run the full pipeline. Bitcoin does this for every fork — SegWit, Taproot, witness-v2 — and now we do too. Soft-fork-able once we’re on chain. -
Coin selection: prefer single notes for fastest ZK proofs
Subtle wallet UX win: when a user has both a 5 BTC note and three 1.66 BTC notes, the wallet would pick whichever combo summed closest to the target output. That’s optimal for change minimisation — but proof generation time scales linearly with input notes, and a 1-input proof is 3-5× faster than a 3-input one on the same hardware.
New rule: prefer the smallest set of notes that sum to ≥ target, breaking ties by largest single note. The user feels the difference immediately — proof goes from 4s to 1s on a M1 MacBook.
-
ESM crypto import in Vite — stop using `require()`
Vite 5 with
"type": "module"rejectsrequire("crypto"). Symptom: dev server boots, prod build silently drops the crypto import, runtime crashes on first signature verify. No warning at build time.Fix:
import { createHash } from "node:crypto"(thenode:prefix matters too — without it, the bundler tries to find a polyfill called “crypto” and ships ~120KB of extra code). Thenode:prefix tells Vite to keep the import as a node-builtin and not try to bundle it. -
Auto-shield mining rewards + atomic-swap CLI
Two features in one commit:
- Auto-shield mining rewards — when a Vanta block matures (30 confirmations), the wallet automatically converts the coinbase into a shielded note. No manual
shield 6.25step. The miner who runs the wallet is now indistinguishable on-chain from any other shielded participant. - Atomic-swap CLI —
zer-cli swap btc:0.001 zer:200opens an HTLC against a remote counterparty, watches both chains for confirmations, and refunds on timeout. First production-quality BTC ↔ ZER UX.
The auto-shield is the one users notice. Half the value of a privacy chain is everybody using the privacy features by default.
- Auto-shield mining rewards — when a Vanta block matures (30 confirmations), the wallet automatically converts the coinbase into a shielded note. No manual
-
Docker + Fly.io seed-node config for ZL1
ZL1 seed-node deploy lands.
Dockerfileis a tiny multi-stage Rust build;fly.tomldeclares 1 GB memory + persistent volume for the chain database + restart policyon-failure.Three Fly gotchas I keep tripping over: (1)
min_machines_running = 1is not the default, and seed nodes need it on; (2)auto_stop_machines = falsefor the same reason — Fly’s eager scale-to-zero will kill your peer; (3) volumes are region-pinned. If you scale to a new region, you get a new volume, and your chain state isn’t there. Always pin the deploy region. -
144 tests across the SDK
Hit 144 tests across the zera-sdk monorepo today. Coverage is decent on
note,commitment,nullifier, and the Groth16 verifier glue; thinner on the Solana-side CPI dispatch, which is where the integration matrix is going to be painful.The shape of the test pyramid is wrong: too many unit tests, not enough end-to-end. Need a
bun test:e2elane that boots a local solana-test-validator + deploys the verifier program + signs a real proof. That’s the next milestone. -
Zera Wallet v3: ZKP core + real data layer
Wallet v3 has the Groth16 proving core wired in, plus a real data layer (no more
localStoragemocks). Note scanning runs against the actual nullifier tree, wallet unlock derives keys from a passphrase via Argon2id.The bit that took the longest wasn’t the proving — that’s a wasm import — it was the note discovery. Every block, the wallet has to try-decrypt every commitment to see which ones are theirs. Trial-decrypt of 1000 notes is ~80ms in the browser; with fuzzy message detection on the roadmap, that drops to <10ms.
-
Data dominates. If you’ve chosen the right data structures and organized things well, the algorithms will almost always be self-evident. Data structures, not algorithms, are central to programming. — Rob Pike
I keep coming back to this every time I’m tempted to be clever in a hot path. Nine times out of ten the win was upstream: a different shape, a different index, a different lifetime. The algorithm only mattered after I stopped fighting the data.
-
x402 protocol security research with confirmed PoCs
Pushed the SOLMAL writeup with confirmed proofs-of-concept against the x402 (HTTP 402 micropayment) protocol. Three issues found, all in the handshake between agent and merchant — the protocol assumes the merchant’s response is unforgeable, which is true if you trust the network layer but isn’t if anyone can MITM the agent’s outbound request.
PoCs land on a mock merchant + mock agent; mitigations need protocol-level signing on the 402 response. Coin Center / EFF style: report responsibly, give vendors 90 days, then publish.
-
P2P bootstrap via master discovery topic + seed nodes
cruiser Phase 30 lands: a master gossip topic that all nodes join on first boot, plus a small set of seed nodes pinned in the binary. Once you’ve heard one peer through the master topic, you can drop into per-app sub-topics and never speak to the master again.
Trade-off: the master topic centralises discovery (everyone hits it once), but at least it’s not a TLS-terminating tracker — just an iroh-gossip topic with the same security model as everything else. Same shape Monero uses for Tor onion peer exchange.
-
Xcode Cloud CI now installs Rust + Node + pnpm
Spent an hour fighting Xcode Cloud CI for the Cruiser+ iOS build. The runner has Xcode and Homebrew but does not have Rust, Node, or pnpm preinstalled — and the default
ci_post_clone.shdoesn’t get them. Fixed with a tiny shell script that installs all three from scratch on every cold start (~90s overhead, acceptable).Lesson: Xcode Cloud is fine for pure-Swift projects. For anything Tauri / iroh / Rust-with-iOS-bindings, GitHub Actions with
actions/runner-images:macos-14gives you a consistent Rust toolchain in 8s instead of 90s. Switching the iOS lane next. -
The bundle ID rename saga
Three commits in 5 minutes:
4e67803rename bundle ID tocom.cruiserplus.app1cc74bdrename bundle ID tocom.cruisergay.app, fix RGBA icons2eed5f8fix Cruiser+ name in Xcode project
Lesson: Apple’s review process treats
cruiserplusandcruisergayvery differently. The first review came back asking us to “clarify the app’s audience”; the second flew through. Names matter at the App Store layer in ways developers don’t expect.Side bonus: Apple’s icon validator rejects PNGs with an alpha channel (“transparency is not allowed for app icons”). One-line
convert in.png -background black -alpha remove icon.pngfixes it. -
Phase 29: iOS via CoreLocation + code signing
Cruiser+ is now a real iOS app. The location stack uses CoreLocation directly (not the cross-platform Tauri location plugin — too generic, too much overhead) for the gay/leather venue heatmap. Two-finger pinch + 60Hz scroll on the map; the latter required
UIScrollView.decelerationRate = .fast.Code signing took the longest. App Store distribution needs a Distribution certificate, not the Development one Xcode auto-creates; provisioning profile for the bundle ID needs explicit “App Store” capability; entitlements file needs
com.apple.developer.location.always-and-when-in-use. None of this is documented in one place. -
Security doc + status analysis for the SDK
Wrote up the SECURITY.md plus a “current status” analysis. The status analysis is the doc I wish more crypto SDKs shipped: an honest table of what’s audited, what’s not, and what’s “implemented but please don’t run this in production yet”.
The threat model section is short. Three lines:
- The SDK assumes the host machine is not compromised.
- The SDK does not protect against rubber-hose cryptanalysis.
- Anyone running this on devnet, sweet. Anyone running this with mainnet money, talk to me first.
-
MCP server package for AI-agent integration
Shipped
@zera-sdk/mcp— a Model Context Protocol server that exposes the SDK’s note/commitment/nullifier API as a JSON-RPC tool surface. AI agents (Claude, GPT, etc.) can nowmintNote,transferShielded,verifyProofdirectly without learning the wire format.Asymmetric tool surface design: the MCP exposes only read + simulate by default. Mutating calls require an explicit
--allow-mutationsflag at server start. Discussed in detail in the asymmetric-tool-surfaces paper. -
ZeraClient + NoteStore high-level wrapper
The bottom-up SDK API was great for crypto folks but hostile to first-time users. Added
ZeraClient— a single class that wraps RPC, prover, and note storage behind ergonomic methods likeclient.send({ to, amount })andclient.balance().NoteStoreis the persistence layer: by default in-memory, optionally backed by IndexedDB (browser) or SQLite (node). The contract is small:add(note),markSpent(nf),unspentNotes(),findByCommitment(cm). Three implementations slot in interchangeably. -
Tree-state client: fetch the Merkle tree from chain
The wallet needs the Merkle root to generate proofs, and ideally the full tree to scan for owned notes. Added
TreeStateClientthat pulls compressed-account state from a Photon RPC, reconstructs the depth-32 Poseidon tree client-side, and verifies the on-chain root matches.Performance: 50K notes reconstructs in ~400ms. Above ~1M and the wallet should switch to lazy path-fetching (only request the path for the leaf you’re spending). That’s a future commit.
-
Phase 28b: native location services for Windows + Linux
The cross-platform location story before this was: macOS used CoreLocation, Windows used IP geolocation, Linux used “open a dialog and let the user type their lat/long”. The first one was great; the other two were embarrassing.
Windows now uses
Windows.Devices.Geolocation.Geolocatorvia WinRT bindings (the Tauri Rust cratewindows-rsmakes this manageable). Linux usesgeoclue2over D-Bus. Both ask for the same permission UX as a browser, both fail to “user typed in 0,0” if denied. -
Simon Willison: the lethal trifecta is finally a meme
Simon’s been hammering on this framing for two years and it’s finally landed: any agent that has private data + untrusted input + ability to exfiltrate is, by construction, a prompt-injection victim waiting to happen.
The new piece adds a clean threat-model checklist that I’m stealing for our internal review template. The screenshot of a Claude desktop integration leaking calendar entries via a poisoned PDF is going to make a lot of execs nervous.
-
Cloudflare build fails: missing type alias
Cloudflare Pages build kept failing with
Type 'HospitalResult' is not exported. Locallyastro checkwas happy. The reason:tsconfig.jsonhad"include": ["src"]but the file with the type lived atsrc/data/types.ts— andgit statusshowed it as untracked because I forgot togit add.Local TS resolves untracked files fine if they’re in the working tree. CI clones from a SHA and only sees what was committed.
git addis part of “writing the code”, not part of “publishing it”. I trip over this maybe once a year. -
TIL: Postgres has a real BIT(n) type
Today I learned Postgres ships with a first-class
BIT(n)andBIT VARYING(n)type — not abytea, not anintyou bit-twiddle yourself. You can&,|,~, and<<directly in SQL.Useful for feature flag bitsets where you want index-friendly equality on the bag, but still want
& mask = maskserver-side.