← Blog

2026-09-02

Sandbox money is not money

Yesterday I ran a smoke test against production. Fresh directory, npm install @txn-dev/sdk, a test-mode key, create two wallets, credit one with sandbox funds, pay the other, issue a spending card, cancel it. Everything passed, which was nice. Then I opened the dashboard and it told me the organization had $49.94 across four wallets.

It didn't. It had four test wallets holding play money that never touched a card. The overview was summing every wallet it could find into one big "total balance" and there was nothing on the page, or the wallets page, or the transactions page, that said which rows were sandbox. Fifty fake dollars, presented with exactly the same weight as fifty real ones.

That's a small bug with a nasty shape, because the thing it gets wrong is the one thing a payments dashboard exists to get right.

How it got there

Test mode is a good idea that's easy to half-finish. Keys have a mode, wallets have a mode, transactions have a mode, and every API route scopes its queries to the mode of the key that called it. That part was solid from day one. What nobody had done was carry the distinction into the human-facing views, because when there's one developer and no customers, all the wallets are yours and you know which is which.

The moment a second person looks at the page, that knowledge is gone. A customer who tried the sandbox and then funded a real wallet would see the two added together and have no way to tell what they could actually withdraw.

What changed

Headline numbers are live money only now. Total balance, active wallets, transactions, today's volume, all of it filters on mode. Sandbox gets its own line underneath, labeled as not real money, and every test wallet and test transaction carries a small amber badge wherever it appears. The totals themselves go through a helper that adds in subunits rather than floats, because a bug about money should be fixed with integer math.

There's a second change that's more of a product decision. On the production domain, sandbox mode is now reserved for admins. A customer creating a key sees only Live. The reasoning is that a platform that moves real money should show a customer nothing but real money, and if we want to exercise the system ourselves we can do that with an admin account and the badges make it obvious what we're looking at.

I'm honestly not sure that last call is right. It makes the product truthful, it also means a developer evaluating the SDK can't try it without a card. The two are in tension and I've picked the side that's harder to get wrong for now.

The general rule I took from it

If your data model has a flag that changes what a number means, every view that shows the number has to show the flag. Filtering the queries isn't enough, the human reading the page needs the same information the query had. Mode, currency, pending versus settled, it's all the same problem, and the fix is always the same: the number never travels without its label.