Wisp 2026 Updated
A payment gateway where the shopper stays private and the merchant still gets paid
A stablecoin checkout with two modes: public, which settles on-chain in one transaction, and private, which routes the payment through a zero-knowledge pool so nothing links the shopper to the shop.
- Role
- Co-founder (with a partner) · frontend lead, backend contributor
- Timeline
- 2026; phase one complete on testnet, not on mainnet yet
- Team
- Small team; the Solidity contract by another engineer
- Stack
- Next.js, TypeScript, Go, Postgres, gRPC
Overview
Two ways to pay, one way to get paid
Wisp is a stablecoin payment gateway. A shop drops in a checkout, a shopper pays in USDC from their own wallet, and the shop is paid without touching a wallet themselves. What makes it a product rather than a transfer button is the choice the shopper gets at checkout: pay in public, the way every on-chain payment works today, or pay privately, through a zero-knowledge pool that leaves nothing on-chain connecting them to the shop.
The merchant side is a working product: a shop signs up, sees its balances, watches payments arrive, registers where private money should land, issues keys, and reads every notification its own server was sent. The shopper side is a checkout that takes a real payment on-chain. Phase one is complete on testnet; it is not on mainnet yet, so there are no real users or volume to report. What is still in front of us is settlement operations: payouts, reconciliation and disputes, and the case study says which is which rather than implying the whole thing is done.
There are four moving parts: a Solidity router that settles public payments in a single transaction, a Go API that owns invoices and the ledger, a Node service that holds everything the privacy layer needs, and the web application that both the shopper and the merchant actually touch. I own the web application end to end, wrote parts of the API, and integrated both sides against the router. The contract itself was written by another engineer on the team.
- 4
- Services on the payment path
- 3
- Independent idempotency layers before money moves
- 0
- Floating point numbers anywhere near money
Problem
Every crypto payment publishes the buyer
A blockchain broadcasts each purchase to anyone watching: the amount, who was paid, and the full history of the paying wallet. Pay a shop on-chain and you have handed that shop, and everyone else, your balance and your entire purchase history. No card network works this way, and no shopper would accept one that did.
The existing fix makes the problem worse. Funds that pass through a mixer are routinely frozen by exchanges and off-ramps, so privacy bought that way is privacy a business cannot use. Meanwhile the merchant wants none of this: not volatility, not custody, not a wallet to look after. They want the money, in the form they chose, on time.
- Privacy that a partner can verifyA payment nobody can trace is only useful if we can still show a regulator or a banking partner where the money came from. Those two requirements have to hold at the same time.
- The shopper signs, so the shopper must be sureEverything a wallet is asked to approve comes from a server response. If a screen says one amount and the wallet signs another, the shopper loses their balance and finds out afterwards.
- Chains disagree about what happenedA block can be reorganised out from under a payment that was already marked paid, and a shop may have been told about it.
- The privacy toolchain is in the wrong languageThe mature zero-knowledge pool ships a TypeScript SDK. The service that owns invoices and money is written in Go.
Discovery
What the privacy layer turned out to be
The first week of the private flow went into finding out what the pool actually requires, and three answers changed the plan.
The first: the shopper's half of a private payment is much smaller than it looks. Moving money into the pool is a shield, and a shield needs no merkle tree, no synced wallet and no zero-knowledge proof. Proving is only required to spend from inside the pool, which is our side of the payment, not theirs. That collapsed the browser's job from a heavy client that downloads proving artifacts to one ordinary transaction, and it is the single biggest reason the checkout is fast on a phone.
The second: the version of the pool we build on has no memo field on a shield. The obvious design, where the shopper attaches the invoice number to their payment, is not available. So the invoice is matched by what the server can observe on its own, and the shopper's client is never trusted to label money it is sending.
The third: privacy pools do not exist on every chain, and the chains the gateway settles on are not automatically among them. That meant the checkout had to know the difference, rather than assume.
System design
Four services, one story about money
One API owns the money. A browser talks to it, That API owns invoices, the ledger and every decision about money; it never lets another service write a balance. Two things it cannot do itself live behind their own boundaries: the chain, which it watches rather than trusts, and the privacy layer, which speaks a language it does not.
Browsers
Edge
Services
Chain and data
Browsers
- CheckoutShopper, no account
- Router contract · the shopper signs and sends
- Same-origin proxy
- Merchant dashboardBalances, payments, keys
- Same-origin proxy
- Shop backendCreates invoices, gets notified
- Session + key auth · server to server
Edge
- Same-origin proxyToken never crosses origins
- Session + key auth
- Session + key authMerchant keys, invoice tokens
- Go API
Services
- Go APIInvoices, quotes, ledger, payouts
- Postgres
- Screening provider
- Chain watcher
- Privacy sidecar · typed contract
- Shop backend · signed notifications
- Chain watcherEvents, confirmations, reorgs
- Router contract · confirmed events
- Postgres
- Privacy sidecarThe only service holding the pool SDK
- Zero-knowledge pool · scan and decrypt
Chain and data
- Router contractVerifies, splits, forwards, once
- PostgresDouble-entry ledger, one source of truth
- Screening providerWallet risk, before private mode
- Zero-knowledge poolPublic infrastructure
Solid lines are request paths. Dashed lines are things that arrive on their own schedule: blocks confirming, a scanner finding a payment, a notification going out to a shop. Note the top line: the shopper's wallet pays the contract directly, so the money never passes through us. A private payment leaves the same way, into the pool, and only the scanner sees where it landed. The browser never talks to the privacy service, and the privacy service never talks to the database.
The router is the whole public payment. The API signs the exact terms of one invoice, the shopper's wallet calls the contract with that signature, and in one transaction the contract checks the signature, separates the fee, and forwards the rest to the merchant's own address. On the public path Wisp never holds the money: it never touches the transfer. A duplicate call on the same invoice reverts, so the contract, not our database, is the last word on whether a payment happened twice.
The privacy sidecar is the answer to a language problem that could have gone badly. The mature pool SDK is TypeScript; the service that owns money is Go. Rewriting cryptography in Go to keep one language was never a real option, and neither was moving the ledger into Node. So the SDK lives alone in a small service that holds the pool wallet, scans for incoming payments and decrypts them, and offers the Go side a typed contract with five calls on it. That service has no database, no authority over balances, and no route in from the internet.
The watcher is deliberately dull. It reads events from a block behind the head, records each one under the chain, transaction and log position it came from, and advances a stored marker so a restart resumes rather than replays. An event only counts once it has the confirmations the chain is configured to need, and an event whose block is later orphaned is marked as such rather than deleted, because the fact that we once believed it is part of the record.
The shop creates an invoice
A server-to-server call opens an invoice against the merchant's account and returns a link. The shopper is sent there; the link itself is the credential, so the checkout needs no account and no login.
On failureAn invoice that has expired, been paid, or been cancelled opens as a closed state rather than a payable one, so a stale link cannot take a second payment.
The wallet is screened before the option exists
Private mode is offered only to a wallet that passed screening, and the report is stored against the invoice. This ordering is the point: the entrance to the pool is controlled, so every private payment we later have to account for came from a wallet we checked first.
On failureA wallet that does not pass is offered the public route instead, without an explanation to work against. A verdict that has not come back yet is not a pass: the public route carries on, and private mode waits for a real answer rather than treating silence as one.
The shopper sees the whole price before anything is signed
Amount, the rate behind it and the fee are on screen before a wallet prompt opens, with the quote counting down to its expiry rather than silently going stale.
The browser checks the server before the wallet does
The terms the wallet will be asked to approve arrive from the API, and are re-checked against exactly what the shopper was shown: the same amount, the same chain, a token from the list that chain published, and a contract from the deployment allowlist. Passed through is not the same as trusted.
On failureA mismatch stops before any wallet prompt. A response that disagrees with the screen is treated as an attack, not as a new price.
The payment is rehearsed before a wallet opens
The exact call the wallet is about to make is simulated against the chain first. If the contract would reject it, the shopper is told that the shop's setup and the network disagree, before they have approved anything. Without that rehearsal the first thing they pay for is an approval, and the payment reverts immediately afterwards.
On failureA rehearsal that cannot run, because the network is unreachable, does not block the payment: an unknown answer is not a refusal.
The shield is one ordinary transaction
The browser derives a one-time key from a signature, builds one note for the merchant and one for the fee, and sends them together in a single transaction addressed into the pool. The key is used and dropped: it is never stored, never sent to us, and cannot spend anything.
On failureA rejected signature, a short balance, or a chain with no pool each end in their own message. A private invoice on a chain without a pool is refused outright rather than quietly settled in public, because a shopper who asked for privacy must never be given publicity instead.
The pool hides what happens next
From outside, a wallet deposited into a public privacy pool, which thousands of wallets do. Separately, a merchant was paid. No direct on-chain link joins the two. Amount and timing correlation are the residual risk, which is why the fee note and the merchant note travel in one transaction rather than two. Matching the shield to its invoice happens on our side from what the scanner sees, not from anything the shopper's client was trusted to attach.
The hash is reported, and the report is not allowed to be the weak link
By the time the browser tells us the shield happened, the money has already moved. So that one call retries with backoff rather than failing with the tab, and a refusal the server means (a bad request) stops immediately instead of hammering it.
On failureA payment whose settlement depends on a browser tab surviving long enough to make an HTTP call is a payment that will sometimes not settle. That is written down as the open risk it is, not smoothed over.
The merchant is paid and told
Settlement is written to the ledger in one transaction with the fee, the shop is notified with a signed message it can verify, and the dashboard shows the payment with no receipt link, because there is no public record to link to. The absence is rendered as a statement rather than an empty cell.
On failureA notification the shop does not acknowledge is retried with backoff and surfaced in the dashboard, so a shop is never silently out of sync about money it has received.
Four stack decisions carry most of the weight here, and each one has a bill attached.
| Call | Why, concretely | What it costs | How that cost is paid |
|---|---|---|---|
| A separate service for the privacy layer | The pool's SDK is TypeScript and the money service is Go. A sidecar keeps each language where it is strongest, and confines the one dependency most likely to change to a service that can be redeployed on its own. | A second runtime, a network hop, and a contract to keep in step between two repositories. | The contract is generated from shared definitions both sides build against, so an incompatible change is caught before it merges rather than at runtime. The hop is internal only, and the sidecar owns no data, so the worst case is a feature pausing rather than money going wrong. |
| Shield in the browser, prove on the server | A shield needs no proof, so the shopper's device does the cheap half and our infrastructure does the expensive half. On a phone that is the difference between a checkout that works and one that times out downloading proving artifacts. | The server carries the heavy cryptography and the pool wallet, which is now something we operate and secure. | The keys that matter live behind a secrets-manager interface rather than in code, and the sidecar is unreachable from outside. The browser never holds anything that can spend. |
| The API signs the terms, the contract verifies them | The shopper's own wallet pays the merchant directly, so nobody has to trust us with the money in flight. A signature over the exact invoice terms is what lets the contract accept that payment without us standing in the middle of it. | A signing key that must be protected, and terms that cannot be edited after signing without invalidating everything. | The signer is one interface with one implementation to swap for a managed key service, and every field is re-checked in the browser against the screen before a wallet is asked to approve. |
| One double-entry ledger, in integers | Balances are summed from entries rather than stored, so nothing can drift from its own history, and every amount is an integer in the currency's smallest unit. | More rows, and arithmetic that has to be written carefully rather than reached for. | The ledger is append-only where a correction would erase evidence, and the money helpers have their own runnable check, so a rounding mistake fails a command rather than a reconciliation. |
Every one of these buys something specific and charges for it somewhere else.

“Zana turns designs into experiences. His frontend work isn't just functional, it's intentional. Every pixel has a reason. Rare to find that level of craft.”
Correctness
Everything that must not happen twice
A payment gateway is mostly the study of things arriving more than once. A shop retries a request it never saw answered. A chain delivers the same event after a restart. A scanner replays a page it already read. Any of those crediting a payment twice is worse than an outage, because an outage is visible.
So repetition is handled three times over, at three different boundaries. An inbound request carries a key that makes a retry return the first answer instead of doing the work again. A ledger movement is written under an identity derived from what it represents, so the same settlement cannot post twice. And a chain event is stored under the chain, transaction and log position it came from, which is a natural identity nothing else can collide with. Each layer would be enough on a good day; the point is that they do not share a failure.
The chain gets the same treatment from the other side. Nothing counts as paid on its first sighting: an event waits for the confirmations that chain requires, and a block that is later orphaned marks its events as reorganised rather than deleting them, because a shop may already have been told and the record needs to show what we believed and when. The watcher keeps a marker per chain so a restart resumes from the last block it finished rather than rescanning from zero or, worse, skipping ahead.
Notifications to a shop are their own small reliability problem. They are signed so the shop can verify the message is ours, retried with backoff when the shop's endpoint is down, and every attempt is recorded with its response, so a merchant can see in the dashboard that their own server rejected a payment notification five times rather than concluding that the payment never happened.
The parts that are easy to get quietly wrong have checks that run as commands rather than opinions: the contract interface the frontend encodes against is asserted to match the deployed one, the money helpers are asserted to round the way the backend does, and the privacy layer's network support is asserted against the SDK's own list rather than a constant someone remembered to update. They run in seconds and fail loudly, which is what makes them useful in a place where a mistake is a wrong amount rather than a broken page.
Decisions
Where the faster option was wrong
Five product decisions took the most argument, and in each case the faster option was the one that would have been discovered later, by a shopper.
| Challenge | Obvious option | What I did, and why | What it cost |
|---|---|---|---|
| A private invoice with nowhere private to go | Settle it in public and tell nobody | Refused the payment with an explanation. A shopper who chose privacy and silently received publicity has been harmed by the product, and no amount of conversion is worth it. The network list is read from the SDK's own configuration, so the day a pool appears on a chain we settle on, it starts working without a code change. | A checkout that sometimes says no, and a rule that has to be explained to a merchant. |
| Server-signed terms the wallet must approve | Pass the signed parameters straight to the wallet | Re-checked every field against what the shopper was actually shown before any prompt opens, and pinned the contract to a deployment allowlist. A compromised or spoofed response would otherwise be able to show one amount and have a wallet sign another. | A second source of truth in the client, which has to be kept in step with the screen. |
| A viewing key that reads every payment, forever | Take it in a form and store it like any field | Treated it as the sharp object it is: write-only at the API, encrypted at rest, never returned by any read, redacted from logs, and the form says in plain words what the key can do and to use a wallet kept for this purpose. The key that can spend never comes near us, which is why the merchant pastes an address they already own instead of being offered a wallet we would have to create. | A rougher onboarding step than generating a wallet for them would have been. |
| A private flow that looked finished but moved no money | Leave it in and label it a demo | Deleted it. The flow faked the wallet signature, animated a proof that was never generated, invented a transaction hash, and told the shopper their payment was private. A screen that lies convincingly is worse than a missing feature, because everyone downstream plans around it. It came back weeks later on the real SDK, and what shipped in between was a refusal that told the truth. | A visible step backwards in a demo, and a feature that read as missing while it was being built properly. |
| A private invoice could still settle in public | Treat the chain picker and the payment mode as separate choices | Closed the path instead. A private invoice is only offered networks that have a pool, is moved onto one if it was created elsewhere, and is refused outright when none of them can take it. The hole was not in the privacy layer at all: it was two correct screens that, used together, published exactly the payment someone had asked to keep private. | Fewer networks on offer for a private payment, and a rule the merchant has to be told about. |
| The pool gives a shield no memo field | Have the client attach the invoice reference | Matched the payment from what our own scanner observes instead. A reference the paying client controls is a number an attacker can also choose, and on a payment gateway that is an invitation to credit someone else's invoice. | More work on our side, and a scanner that has to be watched, because now it is load-bearing. |
One decision was about what not to build. The merchant application shipped without the screens the API could not yet serve, rather than with screens that invented their own contents. A balance nobody can stand behind is the fastest way to lose a partner's trust in everything else on the page, and each of those screens was cheap to add back the day the service behind it existed.
The same instinct went through the copy with a red pen. A payout “expected within 2 to 6 hours”, a shield that takes “a couple of minutes”, a landing page promising five networks in under a minute: none of those numbers had been measured, and a payment product that guesses at timings in writing is teaching people not to believe it. They came out. What is left is what the system can actually stand behind.
The same instinct shaped the checkout copy. The words the industry uses, the pool's name, the screening vocabulary, the compliance terms, appear nowhere in front of a shopper. They see an amount, a fee, a countdown, and a plain sentence about what is about to happen. The interesting machinery is the part a payment product should never make its user learn.
Lessons
What building on a privacy protocol taught me
The cryptography was never the hard part, because it was not mine to write. The hard part was deciding where uncertainty is allowed to live: which service may be wrong, which one is allowed to be slow, and which one is the last word when they disagree. Once the chain was something watched rather than trusted, the pool something reached through one boundary, and the ledger the only place a balance exists, most of the remaining questions answered themselves.
The other lesson was about the boundary between a server and a wallet. In an ordinary product, the server is the authority and the client renders what it is told. When the client is holding a key that can move a stranger's money, that inverts: the client has to check the server, because the person clicking is the one who pays for a lie. Building the checkout as though the API might be hostile made it a better client of a friendly one.

