
Yesterday I posted my first Show HN. It is at two points and two comments as I write this, and I wrote the ending in advance, in the post itself: "I am expecting to 'flop' in here since it's literally my first post. I know, I should feel ashamed!"
The internet read the script and obliged.
Clarifying: I meant ashamed for not having posted more after this many years.
Fine. Best launch I ever did on HN, mostly because it is the only one, and because the two points were never the point. The point was to finish the tool, point it at the world, and see what came back. What came back was one star, one thoughtful stranger, and a collaboration in embryo. We will get there.
First the tool. It matters more. It always does.
Your test suite should not need a Stripe account
I have spent twenty-odd years integrating other people's APIs: commodities trading and market data in London, fintech, payments, blockchain RPC infra. The test story never got better. Sandbox accounts you have to create, seed and babysit. Live secrets in CI, because someone was in a hurry once. Rate limits. Flaky suites, because the mock did not behave like the real thing, so you ended up testing the mock instead of your code.
The usual tools are not wrong. WireMock, Prism, MSW are great at static and schema-shaped mocks. What I always wanted was a runnable stand-in: create, list, mutate, webhooks, auth expiry, cursor pagination, state machines. A Stripe that is actually there when you turn around. Without writing it all myself, again, on every project.
A stunt double, not a stub
In film, the stunt double takes the hits so the star does not have to. That is the whole job description, and it is right there in the name: stunt. Local, stateful stand-ins for the public APIs your code talks to. Ninety-five adapters: Stripe, Twilio, GitHub, S3, Salesforce, and the long tail. Each one is YAML, sandboxed Starlark handlers, and synthetic fixtures, packed into a single Go binary.
brew install --cask stuntapi/tap/stunt, then stunt demo. You get a Stripe-style sim on a local port, and the curl it prints actually means it: create a charge, list it back (it is still there; state persists across requests and restarts), capture it, watch the signed webhook fire at a local sink. All synthetic, all offline, no Stripe account, no bills. Docs here.
And when you want your double to take a hit on purpose:
1version: 12rng_seed: 423network: { mode: port, base_port: 8000 }4services:5 example:6 rules:7 - match: { method: GET, path: /hello }8 when: { chance: 20 }9 respond: { status: 503, body: { inline: { error: boom } } }
One request in five gets boom. Deterministically, because the RNG is seeded. Chaos you can replay in CI, which is the only kind of chaos worth having.
The sandbox is the product
The clever part is not the adapters. It is where they run. Adapter logic executes in a Starlark VM with no host I/O and no network, with bounded execution. That is the property that makes it safe to stunt catalog add a stranger's adapter, the way you would install an npm package: third-party logic, zero trust required.
State lives in engine primitives, not in the scripts: SQLite-backed collections, KV, a blob store, HMAC tokens, a webhook emitter that signs with the real provider schemes. Everything is deterministic: an injectable clock, a seeded RNG (Random Numbers Generator), snapshot and restore. Each server gets a dashboard with a request inspector that does copy-as-curl and replay, so when a test fails you interrogate the crime scene instead of imagining it.
What belongs in the double
Jeremy, the one substantive commenter, asked which provider behaviours are worth promoting into reusable contracts. My answer, warmed over: anything a client cannot distinguish in a test suite belongs in the mock. State machines, pagination, signature schemes. The semantics stay provider-native. A Stripe refund and an Adyen reversal are not the same object, and abstracting them is how payment integrations get burned.
In other words: "fake the physics", never the plot.
Real SDKs keep the adapters honest
Any mock can pass tests you wrote yourself, in a language of nods and winks. The bar I care about is fooling the real client libraries. So CI drives the actual provider SDKs against the adapters: stripe-go, aws-sdk-go-v2 (real SigV4), go-github, twilio-go, go-shopify, and the Node trio of stripe, octokit and twilio, all through the real binary.
That harness caught real bugs in my adapters. Stripe money fields coming back as JSON strings, which typed SDKs simply reject. GitHub pagination Link headers pointing at production: octokit followed them straight to the real api.github.com, from inside a test that thought it was offline. Both bugs looked fine to my own tests. The SDKs were not fooled, and that is exactly the point of them.
The honest limits
- Adapters are unofficial and fidelity-ranked, not API-complete (contributions welcome). I would rather nail the flows real integrations actually exercise than checkbox every endpoint.
- Windows is a script install rather than the default winget source. Yet.
- If you integrate an API that is missing,
stunt adapter import openapi|har|protoscaffolds one. PRs welcome, genuinely.
Two points, one star, one collaborator
Back to the flop. Here is what two points actually bought:
One star, the first. One comment, from Jeremy, that engages with the sandbox design on its merits, points at an adjacent project of his own (Vyral: live-provider qualification, complementary rather than competing), and floats a collaboration. By the end of the thread we were, in my own words to him, "already collaborating in a way" because I dared to hand him a suggestion for his project. Hopefully it's going to be well received... that's the same "shipping" spirit I want to hold optimistically going forward.
My working theory of the flop, honestly held: a first post from an account with no history gets no lift, so almost nobody saw it; the title asks you to parse a metaphor before the payoff; and "95 adapters" in parentheses reads as a list nobody counts. I priced all of it in and posted anyway, because a launch is one hour of one day. The repo, the docs, and now this post are the durable artifacts. They keep working after the thread goes cold, and they are how the next hundred readers trickle in.
Also: I asked for fresh virtual tomatoes, and the internet sent one polite collaborator instead. Gentlest tomatoing imaginable.
But I need tomatoes to make a salad! I want criticism, ideas, opportunities for growth!
I wrote about the artisan and his Mk 45 chisel the other day: the tools are never finished, only current. stunt is v0.3.0, three hundred and fifty-seven commits in. In artisan terms that is Mk 357 wearing a modest name tag. It earns its keep in my own daily work or it does not; that is the only metric that was ever real.
Adapter #96 is taking nominations. wintty is probably the next shipped product from solo team yours truly.
Ciao!