The first version of Cards402 onboarded agents the same way every API has onboarded developers since the early 2000s: an operator copies a raw API key out of the dashboard and pastes it into the agent. When the agent in question is an LLM, this turns out to be a terrible idea for reasons that aren't obvious until you've watched an operator do it. Claim codes are the fix — a one-time secret that can be pasted safely because it becomes worthless the moment the agent redeems it.
The failure mode
Picture an operator using a Claude Desktop or Cursor agent to set up a new Cards402 integration. They mint an API key in the dashboard, copy it to their clipboard, and paste it into the conversation with the assistant. The assistant stores it, builds the integration, and does something correct with it.
The key is now in the conversation transcript. That transcript is probably persisted locally. It may be synced to the provider's servers for chat history. It might be quoted in a bug report when the operator screenshots the conversation. It might end up in a vector store as part of a RAG index the operator didn't remember setting up. It will almost certainly show up in any future conversation that references "last week's chat".
Once a long-lived credential enters a transcript, it's effectively public. You have to rotate it, and you have to rotate every such credential every time anyone screenshots anything. That's not a workflow — that's a continuous revocation incident.
The constraint
Agent onboarding has a real constraint that developer onboarding doesn't: the credential has to arrive on the agent's side without the agent's operator ever seeing it in readable form, because the operator is going to paste whatever they see into an LLM chat.
You can't just tell the operator to “handle the credential out-of-band”. “Out of band” is the band an LLM operator is already in. The fix has to be structural: whatever the operator copies has to either be safe-to-paste or transparently worthless.
What we looked at
Three options got serious consideration:
OAuth.Agents initiate a device-code flow, operator approves it in a browser, no credential ever leaves our server. This is the right answer for long-lived web apps. It was the wrong answer for Cards402 because the agent doesn't have a browser — it runs in an MCP server or a headless script — and because device-code flows add a multi- minute human-in-the-loop step to what should be a one-command setup.
Environment variables only.Tell operators never to paste the key into the conversation, only into their shell's environment. This is what every API docs site tells you to do today. It doesn't work. The operator will paste it into the chat the first time they hit any setup issue and ask the agent for help. We tested this with real users; the “don't paste” instruction has about a 15% success rate.
Short-lived exchange tokens. Mint a one-time code that the agent redeems for a real API key on first startup. The operator only ever sees the code; the real credential never exists in a place they can copy. This is what we shipped.
The claim-code flow
A claim code is a string that looks like c402_ followed by hex characters. Three properties:
- Single-use. The first redemption atomically transitions the row from unused to used, and the real API key is sealed so it can never be re-extracted even with database access later.
- Short TTL.Claim codes expire by default — an operator who mints one and forgets about it can't leave a usable credential sitting around indefinitely.
- Worthless after redemption.Even if the claim ends up in a chat transcript that's backed up to a vector store forever, re-pasting it later gets an
invalid_claimerror. There's nothing to rotate.
The operator runs cards402 onboard --claim c402_...exactly once, inside the agent's runtime. The CLI trades the claim for the real API key over HTTPS, writes the key to ~/.cards402/config.jsonwith 0600 permissions, and asks the agent to confirm its setup before the flow is complete. The operator never sees the real API key — it doesn't leave the backend until it's written to disk on the agent's machine.
The transcript-safe property
The whole design hinges on a simple property: any secret the operator is going to paste into the LLM chat has to become worthless within seconds of being generated. Claim codes have exactly that property, because redemption happens as part of onboarding and the claim is atomically consumed on first use.
In the worst case — the operator mints a claim, posts a screenshot to Twitter, and an attacker sees it before the agent redeems it — the attacker has a narrow window to race the legitimate agent to the backend. They win, they steal an API key; the agent loses and has to re-mint. But that race is visible (the operator sees “claim already redeemed” the moment their agent tries), so the fallout is bounded: one re-mint, one revocation, and an operator who has now learned not to screenshot live claims.
Compare that to the raw-key failure mode: an attacker who scrapes a key from a transcript has indefinite use of it until someone independently notices something wrong.
What else it unlocks
Building the claim-code primitive turned out to have side benefits we didn't design for:
- Labels flow through.The claim row carries metadata the operator picked — a label, a spend limit, an optional webhook URL. That metadata is attached to the real API key the instant it's minted, so the dashboard shows the correct label from the first second the agent is alive. No “rename your key after creating it” step.
- Agent state lights up immediately.The backend flips the key's state to
initializingthe instant the claim redeems, so the dashboard onboarding modal progresses even before the agent's own heartbeat lands. Claim redemption is itself a trusted first milestone. - Sealed payloads. The sealed claim row carries not just the api key but also the webhook secret and any other per-agent secrets an operator configured up-front. Everything an agent needs to be fully set up is in one transactional exchange.
What this doesn't solve
Claim codes solve the onboarding problem. They do not solve:
- Ongoing agent compromise.Once an agent has the real API key on disk, a compromise of the agent machine gives the attacker the key. That's a separate problem; the fix is short-lived API keys with refresh, which is on the roadmap.
- Operator who copy-pastes the config file. If an operator opens
~/.cards402/config.jsonand pastes the contents into the LLM chat, we're back to the original failure mode. We can't structurally prevent this, only make it obviously silly. - Long-tail key rotation.Claim codes onboard once. They don't rotate keys for long-running agents. The dashboard revoke-and-reissue flow is the manual backup, and the roadmap has automated rotation.
The broader lesson
Every piece of infrastructure built for humans has at least one assumption that breaks when the “user” is an LLM operating on behalf of a human. Raw API keys aren't insecure — they're insecure when the operator is going to paste them into a transcript. Webhooks aren't fragile — they're fragile when the receiver is behind consumer NAT. Polling isn't slow — it's slow when every poll burns a rate-limit slot for data the caller already has.
Cards402 is the result of auditing each of those assumptions one at a time and fixing them structurally. Claim codes fix the onboarding one. The other two we've covered in why SSE beats polling and non-custodial card issuance on Soroban.
If you're thinking about agent onboarding and would like to compare notes, email security@cards402.com. We read every one.