ChangeGamer

← All guides · Selling to AI agents

Issuing API Keys to AI Agents Automatically: A Build Guide

Part 5 of Selling to AI agents · 1,626 words · published 2026-07-31 · updated 2026-07-31 · Markdown variant

How to design a system that mints and delivers API keys to agent and software buyers with minimal human friction: trigger models, storage, delivery, key format, tiering, rotation and revocation — illustrated with ChangeGamer's own Stripe-webhook mechanism.

In short

  • Three trigger models exist for minting a key — manual approval, instant self-serve on signup, and payment-confirmed auto-mint — and only the third removes a human from the loop after checkout, which is the shape an agent buyer needs.
  • A minted key needs at least 128 bits of entropy from a cryptographic random source; ChangeGamer mints 160 bits (20 bytes from crypto.getRandomValues) formatted as cg_ plus 40 hex characters.
  • Delivery for a machine buyer should be a pollable endpoint an agent can GET and parse as JSON, not an inbox a script cannot read — ChangeGamer's webhook writes the key to KV on payment confirmation, and a client polls /key?session_id= until it returns 200 instead of 404.
  • Storing keys as plaintext values in a key-value store keyed by the token itself is a real, working pattern — that is what ChangeGamer does — but it is a tradeoff against hashing at rest, and worth naming as one rather than presenting as best practice.
  • Automatic revocation needs a second webhook, not just the minting one: ChangeGamer maps each subscription id to its token at mint time so a customer.subscription.deleted event can delete the token without any operator action at churn time.
  • Payment-confirmed auto-mint still is not a keyless, fully agent-autonomous flow — it collapses the human step to one hosted checkout page, once, not to zero.

Part of the How to Sell to AI Agents: The Complete Guide to Machine Buyers guide.


Everything downstream of a purchase decision — the 402 response, the payment page, the checkout flow itself — assumes the buyer eventually needs a credential to use. This article covers the step most guides skip: how that credential actually gets minted and handed over once payment lands, with as little human involvement as the trigger allows. It sits inside selling to AI agents, the pillar for this cluster, as the operator-implementation layer underneath the payment decisions covered in ACP vs. AP2 vs. x402 and accepting x402 payments.

Which trigger model actually removes the human

Three trigger models cover almost every key-issuance system, and only one removes a human from the loop after the buyer has paid once. Manual approval means a person reviews each request and mints the key by hand — fine for a handful of enterprise deals, unworkable at volume, and the worst fit for an agent buyer expecting a synchronous response. Instant self-serve mints a key the moment someone signs up, with no payment gate — simple, but it only applies to free tiers, since there is nothing to confirm before minting. Payment-confirmed auto-mint sits between them: a payment processor's webhook fires on confirmed payment and itself mints the key and writes it to storage, with no person touching the mint step. This is the model for agent and software buyers, because it is the only one where the entire path after the human's one checkout action runs unattended.

ChangeGamer runs payment-confirmed auto-mint. Its handleStripeWebhook function listens for checkout.session.completed and checkout.session.async_payment_succeeded events; on either, if the payment status is paid, it calls a mintKey() function and writes the result to storage in the same request — no queue, no manual review, no separate provisioning job (worker/index.ts, webhook route registered at line 1253, fulfillment event check at line 441). The only human step anywhere in the flow is completing Stripe Checkout itself, which is a hosted payment page — there is no evidence in this codebase of an agentic-checkout integration that lets a piece of software alone complete that step. State that limitation plainly rather than implying the whole funnel is agent-autonomous: it collapses the human involvement to one action, not to zero.

Where to store the key: plaintext KV vs. a hashed lookup table

The two real options are storing the key as a plaintext value your application can read back directly, or storing only a hash of it and verifying incoming keys by re-hashing and comparing. Plaintext-value storage is simpler: write token -> metadata once, and every subsequent request just checks whether that token exists. ChangeGamer's isKeyValid function does precisely this — it treats a non-null KV read for the trimmed key as valid, with no hashing or comparison step (worker/index.ts, lines 118–126) — and the key entry itself, written at mint time, is the plaintext JSON blob { email, session, created, source, tier, payment_link_id? } stored under the token as the KV key (lines 495–509).

The tradeoff is real, not cosmetic. Whoever can read that store — an operator, a leaked read credential, a misconfigured export — sees live, usable keys directly. Hashing at rest (store sha256(token), verify by re-hashing an incoming key and comparing) removes that exposure, at the cost of one extra step per validation and the inability to display a key back to a user after first delivery. A broadly-readable store across a team or CI pipeline argues for hashing; a narrowly-scoped store where only the issuing worker has read access, as ChangeGamer's KV binding is, is a smaller but still real risk. Decide deliberately rather than defaulting to whichever tutorial you followed.

Delivery: a pollable endpoint an agent can parse, not an inbox it cannot read

A machine buyer needs the key returned somewhere a script can fetch and parse as JSON — a human inbox is a dead end for anything that is not, itself, a person reading email. The pattern that satisfies both a human and a script at once is webhook-triggered mint plus a client-pollable delivery endpoint: the payment redirect sends the buyer (or the buyer's agent) to a URL carrying the checkout session id, and that URL is polled until the webhook-written key shows up.

ChangeGamer's /key?session_id=<cs_...> endpoint is exactly this. Stripe redirects the buyer there immediately after payment; the endpoint validates the session id against ^cs_(live|test)_[A-Za-z0-9]+$ with a 200-character cap (worker/index.ts, lines 545–546, 574–583), then looks up a session:<id> record. If the webhook has not yet landed, it returns 404 with an explicit retry instruction rather than a bare error (lines 594–609) — a script can treat that 404 as "poll again shortly" instead of a hard failure. Once the session record exists, the endpoint returns 200 with the key, its creation timestamp, its tier, and ready-to-use header formats for both Authorization: Bearer and x-api-key (lines 631–646), and every response carries Cache-Control: no-store so nothing caches a stale or empty answer. A human-only email fallback exists for buyers whose self-serve flow does not resolve — reply to the Stripe receipt email — but the primary rail is the pollable endpoint a script can drive end to end (documented in paying for access).

Key format and entropy

A machine-issued key needs enough random entropy that guessing or brute-forcing it is infeasible, generated from a cryptographic random source — never a counter, a timestamp, or a non-cryptographic random function. 128 bits is the commonly cited floor for a bearer credential; ChangeGamer mints 160 bits, drawing 20 bytes from crypto.getRandomValues and hex-encoding them behind a cg_ prefix, for a 43-character token (mintKey(), worker/index.ts lines 340–345). The prefix itself is not a security feature — it does nothing to the entropy — but it makes keys instantly greppable in logs, config files, and secret scanners, which is worth the three extra characters on its own.

Scoping by tier

A key's tier should be resolved from the purchase itself, not asserted by the client presenting it. ChangeGamer resolves the tier at mint time from the Stripe payment link used, writes it into the stored key entry, and re-checks it on every request through a keyTier() lookup against a fixed KNOWN_TIERS list (starter, corpus, enterprise, corpus_annual — worker/index.ts line 128); a legacy or malformed tier value resolves down to starter, the lowest tier, rather than up — a fail-safe default worth copying regardless of your own tier names. The tiers themselves are the subject of access and pricing; the point here is narrower: resolve and store tier at mint time, so a downstream request only ever reads it back, never re-derives it.

Rotation and revocation

Revocation needs its own event handler wired to your payment processor's cancellation webhook — minting a key and revoking it are two different triggers, and a system that only implements the first leaves every cancelled subscriber with a working key indefinitely. ChangeGamer's mint step writes a second mapping, subscription:<id> -> token, whenever the checkout carries a Stripe subscription id (worker/index.ts, lines 516–521); a separate customer.subscription.deleted handler reads that mapping, deletes both the token and the mapping, and writes a revoked:<id> audit record naming the reason (handleSubscriptionDeleted, lines 347–380). That path is dormant unless the Stripe endpoint is also subscribed to the customer.subscription.deleted event — a one-time dashboard setting an operator has to remember to make, not something the webhook code can enforce on its own (lines 432–435). Rotation — issuing a new key and invalidating the old one on request, rather than on cancellation — is a related but separate capability that nothing in this codebase implements today; treat it as a gap if your own system needs buyers to rotate a compromised key without contacting support.

Per-key rate limiting

A tier check answers whether a key may access a resource at all, not how fast it may call. Those are different controls, and conflating them leaves no defense against a single valid key being hammered by a misbehaving agent loop or shared beyond its buyer. ChangeGamer's worker performs the tier check on every gated request but has no rate-limiting layer on top of it — no per-key counter, no sliding window, no 429 path in the code reviewed for this article. That is a real gap in the reference implementation, not a pattern to copy; a production system selling to agent traffic at volume should add a per-key counter — a KV or Durable Object counter with a TTL window is a common Cloudflare Workers approach — before it needs one.

One layer up from static keys is short-lived, cryptographically attested identity — see agent identity and authentication and, for the MCP-specific OAuth 2.1 flow, MCP server authentication. Both authenticate which agent is calling, continuously; static API-key issuance authenticates that a purchase happened, once, and hands back a credential valid until revoked — a system selling metered or high-value access may eventually need both. What a live key is then allowed to spend is a separate governance question, covered in agent spend controls.

What to build first

Sequence it as: pick payment-confirmed auto-mint as your trigger, generate keys with at least 128 bits of cryptographic entropy behind a greppable prefix, decide plaintext-vs-hashed storage deliberately, build delivery to return machine-parseable JSON with an explicit "not ready yet" response rather than a bare error, resolve and store tier at mint time, and wire the cancellation webhook to revocation before launch — not after the first refund request teaches you it was missing. Rate limiting and self-serve rotation can follow once real traffic tells you which one you need first. The exact request/response contract for ChangeGamer's own /key endpoint — every field, every status code — is documented in paying for access: the HTTP 402 flow, a concrete example of the whole loop closed end to end.

Frequently asked questions

Can an AI agent get an API key without any human involvement at all?
Not with a payment-triggered key-issuance system built on a hosted checkout page — Stripe Checkout, and equivalents, require a person to complete the payment form once. What can be fully automatic is everything after that: the mint, the storage write, and the delivery back to whatever polls for it. A fully keyless, agent-paid flow exists only under protocols like x402, where the agent's own wallet signs a payment authorization directly — a different mechanism covered in [accepting x402 payments](/articles/accepting-x402-payments), not the API-key model this article covers.
Should API keys be hashed before storing them, or is plaintext acceptable?
Hashing at rest (so a database or KV read alone cannot yield a usable key) is the safer default, especially for a store with broad read access. Plaintext-value storage, keyed by the token itself, is simpler to implement and is what ChangeGamer runs in production today — the key is the KV key, and a lookup either finds it or does not. That tradeoff is honest to name rather than paper over: it works, but a compromised read credential on the store exposes live keys directly, which a hashed design would not.
What format should a machine-issued API key take?
A short, greppable prefix followed by a long random hex or base62 string generated from a cryptographic source — never `Math.random()` or a sequential counter. ChangeGamer uses `cg_` plus 40 hex characters, drawn from 20 bytes (160 bits) of `crypto.getRandomValues` output, comfortably above the 128-bit floor generally recommended for bearer tokens.
How do you revoke a key automatically when a subscription is cancelled?
Subscribe your payment processor's webhook to the cancellation event (Stripe's `customer.subscription.deleted`, for example) and keep a subscription-id-to-token mapping written at mint time, so the cancellation event can look up and delete the right token without a human searching for it. ChangeGamer does exactly this: the mint step writes a `subscription:<id>` record pointing at the token, and the deletion handler reads it, deletes the token, and leaves an audit record behind.
Should I rate-limit each API key, not just each IP address?
Per-key limits are the more precise control for a paid API — a shared key or a runaway agent loop shows up on the key's own usage, not diluted across a NAT'd IP range. ChangeGamer's own worker does not currently implement per-key rate limiting on top of the tier check; treat that as a gap to close before scaling paid traffic, not as a pattern to copy.

#api-keys #agents #automation #payments #provisioning #monetization

Agents: this guide is available as Markdown and JSON; the whole cluster is indexed at /api/articles.json. The reference corpus behind it is at /llms.txt, with licensing at pricing.