Coin Droppa is a Polygon airdrop arcade — real contracts, real MATIC, a signed jackpot roll dressed up as a retro cabinet floor. In May, the security pass landed: EIP-712 typed-data signing to replace a plain personal_sign, a CSPRNG jackpot roll to replace Math.random(), per-claim nonces with a five-minute expiry, generic error responses to kill the side-channel that told scrapers exactly why a request failed. A SECURITY.md documented all of it as a public threat model, attacker taxonomy included. Canary tripwires went in too — decoy cabinet addresses that don't exist on-chain, honeypot paths that look like a misconfigured deploy, a ledger endpoint borrowed straight from the Thinkst Canarytokens playbook: silent to the attacker, loud to the operator.
None of it ever shipped.
The site that looked fine
The production site had been serving a build from May 7 since May 7. Every deploy attempted after that — four of them, spread across four months — failed. Nobody noticed, because a failed deploy doesn't take a site down. It just leaves the last successful one running. The homepage loaded. The arcade animated. The wallet button worked. There was no error page, no downtime alert, nothing that would make a person check.
The tell, when I went looking, wasn't visible from the site at all. It was in the response headers. Every path — the homepage, a canary status page, a completely made-up URL I typed to see what would happen — came back HTTP 200 with the identical etag and the identical content-length. A real 404 has a different shape than that. A working set of serverless functions has a different shape than that. What I was looking at was a single static file, index.html, catching every route through an SPA fallback rule, including the ones that were supposed to hit real backend functions. The signing endpoint that issues jackpot signatures — the one actual piece of infrastructure a live wallet claim depends on — was one of the routes getting swallowed.
Reading the actual failure
The build log named it precisely, once I pulled it:
Scanning for secrets in code and build output.
Scanning complete. 51 file(s) scanned. Secrets scanning found 1 instance(s)
of secrets in build output or repo code.
Secret env var "BADGE_CONTRACT_ADDRESS"'s value detected:
found value at line 13 in OPERATIONS_GUIDE.md
found value at line 125 in README.md
found value at line 6 in app.js
found value at line 40 in netlify/functions/orchestrator.js
BADGE_CONTRACT_ADDRESS is a deployed contract's address on Polygon mainnet. It's meant to be public — it's printed in the README's own deployment-addresses section, on purpose, so anyone can verify the contract independently. Netlify's secret scanner had it registered as a sensitive environment variable, and its scanner does something specific: it checks whether the value of every configured env var shows up anywhere in the files about to ship. A contract address is, structurally, indistinguishable from a leaked API key to that kind of check — it's just a string that matches. The scanner did exactly what it was built to do. It was checking the wrong thing.
