skip to content
Skill Issue Dev | Dax the Dev
search

Notes

Shorter than a post, longer than a tweet. TILs, links worth commenting on, and the occasional small rant. Notes-only RSS →

  • permalink

    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/count every 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.000004percompressedaccountvs0.000004 per compressed account vs 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_authority had 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.sh script now copies the right Rust frameworks into Frameworks/ so notarization stops failing on missing dylibs. v2 test renames so vanta_v2_*.rs reads consistently with the vanta-node-v2 rust crate name.

    The thing nobody tells you about Tauri 2.x sidecars: the binary name in tauri.conf.json must match the target triple-suffixed binary on disk. bitcoind-aarch64-apple-darwin is what the loader looks for; bitcoind alone 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 bitcoind on 0.0.0.0:8332. Always bind to 127.0.0.1:8332 and 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 /prove endpoint 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=1 for 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 to 30 for 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 /submit endpoint 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 after validate_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.rs was pointing at the old program; now derived from ZERA_NETWORK env 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 = 997 so 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" rejects require("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" (the node: prefix matters too — without it, the bundler tries to find a polyfill called “crypto” and ships ~120KB of extra code). The node: 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:

    1. 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.25 step. The miner who runs the wallet is now indistinguishable on-chain from any other shielded participant.
    2. Atomic-swap CLIzer-cli swap btc:0.001 zer:200 opens 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.

  • Docker + Fly.io seed-node config for ZL1

    ZL1 seed-node deploy lands. Dockerfile is a tiny multi-stage Rust build; fly.toml declares 1 GB memory + persistent volume for the chain database + restart policy on-failure.

    Three Fly gotchas I keep tripping over: (1) min_machines_running = 1 is not the default, and seed nodes need it on; (2) auto_stop_machines = false for 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:e2e lane 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 localStorage mocks). 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.

  • permalink

    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.sh doesn’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-14 gives 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:

    • 4e67803 rename bundle ID to com.cruiserplus.app
    • 1cc74bd rename bundle ID to com.cruisergay.app, fix RGBA icons
    • 2eed5f8 fix Cruiser+ name in Xcode project

    Lesson: Apple’s review process treats cruiserplus and cruisergay very 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.png fixes 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:

    1. The SDK assumes the host machine is not compromised.
    2. The SDK does not protect against rubber-hose cryptanalysis.
    3. 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 now mintNote, transferShielded, verifyProof directly without learning the wire format.

    Asymmetric tool surface design: the MCP exposes only read + simulate by default. Mutating calls require an explicit --allow-mutations flag 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 like client.send({ to, amount }) and client.balance().

    NoteStore is 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 TreeStateClient that 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.Geolocator via WinRT bindings (the Tauri Rust crate windows-rs makes this manageable). Linux uses geoclue2 over 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. Locally astro check was happy. The reason: tsconfig.json had "include": ["src"] but the file with the type lived at src/data/types.ts — and git status showed it as untracked because I forgot to git 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 add is part of “writing the code”, not part of “publishing it”. I trip over this maybe once a year.

  • permalink

    TIL: Postgres has a real BIT(n) type

    Today I learned Postgres ships with a first-class BIT(n) and BIT VARYING(n) type — not a bytea, not an int you 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 = mask server-side.