- ›
An autonomous ad buyer has to spend real money— cloud, ad networks, data, tools. The moment you let an LLM agent touch a shared corporate card, one runaway prompt can drain your float and every charge is indistinguishable from yours. - ›
Naïve ships the hard part as a primitive: per-agent virtual cards issued against a KYC-verified cardholder, each with a spending_limit_cents ceiling the card network enforces at authorization time— not a number you check in your own code. - ›Each client is a tenant user under an Account Kit that gates the cards primitive at execution time. Card issuance and top-up are approval-gated, so minting money or raising a cap freezes for a human.
- ›
A card is assigned to exactly one agent. Only the assigned agent can pull the PAN/CVC, and unassigning revokes it instantly— the credential is scoped, not shared. - ›Enforcement composes and runs server-side: a kit with cards disabled makes every card call throw forbidden (primitive_disabled_by_kit); a swipe over the remaining balance is declined at the network; a prepaid card over $150 is rejected as invalid_input; and every charge is logged per client for reconciliation.
- ›You write the media-buying logic. Naïve enforces who the cardholder is, how much each agent can spend, which agent holds the card, and what needs a human before funds move.
Most agent demos stop at the point where money changes hands. They can research a campaign, draft the creative, even pick the audience — then they hand a human a to-do: go enter a card number on the ad network. The autonomous part ends exactly where the spend begins.
An ad-buying platform can't stop there. Buying media is the job:
- Cloud, data, and tooling the agent needs to run.
- Ad spend on real networks — Google Ads, Meta, wherever the client's audience is.
- Recurring subscriptions the campaign depends on.
So the agent has to spend real money, and that is exactly where a naive build falls apart. The moment you have more than one client, handing agents a shared corporate card means:
- Every agent charge is indistinguishable from yours — reconciliation and per-client P&L become guesswork.
- There is no per-agent ceiling — one runaway or prompt-injected agent can spend the entire limit before you notice.
- There is no cardholder of record per client — high-risk merchant categories decline the card.
That plumbing — verified cardholders, per-agent cards, network-enforced spend caps — is the product. It is also exactly what Naïve ships as a primitive: cards. This is a developer tutorial: every code block runs against the current @usenaive-sdk/server, with signatures pulled straight from the docs.
What we're building
- A multi-tenant backend where each client gets one or more buyer agents, each holding its own virtual card with its own hard ceiling.
- Issue — the agent asks for a card; issuance is approval-gated, so a human signs off before money is minted. Funding is a one-time checkout; from then on the agent operates inside the ceiling.
- Scope — the card is assigned to exactly one agent. Only that agent can pull the PAN/CVC to spend; unassigning revokes it instantly.
- Spend & prove — the agent buys at any Visa-accepting merchant, then logs every charge for per-client reconciliation.
- The moat lever is execution-time spend authority: a ceiling the card network enforces at swipe time, a KYC cardholder of record, per-agent assignment, kit gating, and approval on issuance and top-up.
The primitives in play, all from the docs:
| Primitive | Role in the platform | Docs |
|---|---|---|
| Users | One tenant user per client — the isolation boundary | naive.users |
| Account Kits | The plan: is cards enabled, and what needs approval | naive.accountKits |
| Cards | Per-agent virtual cards with an issuer-enforced ceiling | forUser(id).cards |
| KYC / Verification | The cardholder of record behind managed virtual cards | /cards/cardholder |
| Approvals | Human-in-the-loop on issuing cards and raising caps | forUser(id).approvals |
| Logs | Per-client audit trail of every charge | forUser(id).logs |
The card runs on real rails — a managed virtual card on the Visa/Mastercard network, funded through a hosted checkout — so it spends anywhere your client's ad networks accept a card:
Step 0: Install and authenticate
You need a Naïve workspace key (nv_sk_...) from Studio → Settings → API keys. The SDK is server-only — keys are server secrets, never shipped to the browser.
npm install @usenaive-sdk/serverimport { Naive, NaiveError, isPendingApproval } from "@usenaive-sdk/server";
const naive = new Naive({ apiKey: process.env.NAIVE_API_KEY! });Two surfaces matter, and the split is the whole design:
naive.*(the root client) is the control plane —naive.users,naive.accountKits— plus the company-level cardholder.naive.forUser(id).*is the data plane (cards,approvals,logs) bound to one client.
Read naive.forUser(clientId) as: "issue and operate a card, as this client, under this client's plan." Acme's buyer agent is naive.forUser(acmeId); it has no path to Globex's cards.
Step 1: The plan — an Account Kit that gates spend
An Account Kit is a reusable policy template. Cards are a Tools primitive, so the kit decides whether a client's agent can issue cards at all — and whether issuing one or topping it up needs a human first.
// The buyer plan: cards on, but minting money and raising caps freeze for a human.
const buyerKit = await naive.accountKits.create({
name: "Ad Buyer",
primitives_config: {
cards: { enabled: true, requiresApproval: true }, // create + top-up freeze
},
});Cards default to requiring approval for agent calls, so requiresApproval: true is the safe, explicit default here (docs). Calls on the account's own default agent profile still execute without approval — it is agent actions under a gated kit that freeze.
Step 2: Provision a client and its cardholder
Each client is a tenant user. Tenant users never sign into Naïve — you manage them entirely through the SDK.
const acme = await naive.users.create({
external_id: "client_acme", // your stable id for the client
email: "ops@acme.example",
label: "Acme Inc",
account_kit_id: buyerKit.id,
});
const acmeClient = naive.forUser(acme.id);Managed virtual cards — the full virtual Visa with no spending maximum — are issued against a cardholder, which you create once per company. This registers the identity of record for KYC compliance, and it is what makes the card accepted at real merchants (docs).
// One-time, per company. Prepaid gift cards do NOT need this step.
const res = await fetch("https://api.usenaive.ai/v1/cards/cardholder", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NAIVE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
firstName: "John",
lastName: "Doe",
email: "john@acme.com",
billingLine1: "123 Main St",
billingCity: "San Francisco",
billingState: "CA",
billingPostalCode: "94105",
dobDay: 15,
dobMonth: 6,
dobYear: 1990,
}),
});
const { cardholder } = await res.json();/kyc and /formation when the cardholder should be a verified principal inside a company you formed through Naïve.
Step 3: Issue a scoped card
Now the agent asks for a card. Issuance is the high-consequence moment — you are minting spending power — so cards.create is approval-gated by our kit. It returns either the created card or a PendingApproval (HTTP 202); it does not throw (errors).
const created = await acmeClient.cards.create({
name: "Acme — Search Ads",
spendingLimitCents: 250000, // $2,500 hard ceiling on this card
});
if (isPendingApproval(created)) {
// Frozen for a human. Approving replays the action and issues the card.
await acmeClient.approvals.approve(created.approval_id);
}Under requiresApproval, the flow is deliberate: the agent requests the card, a human approves the budget, and only then is a card object created — carrying a checkout_url for its initial funding.
// After approval, the created card carries a checkout_url for one-time funding.
const card = await acmeClient.cards.get(created.id);
// card.status === "pending_payment" until fundedFunding is a one-time hosted checkout — open the checkout_url to load the card. After payment, issue it and confirm it went active:
await fetch(`https://api.usenaive.ai/v1/cards/${card.id}/check-payment`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.NAIVE_API_KEY}` },
});
// On success the card is issued automatically and status becomes "active".The card now has a fixed ceiling. From here the agent operates inside it with no further human touch — until it wants more budget, which brings us back through approval (Step 6).
Step 4: Assign the card to one agent, then spend at execution time
A client can run several buyer agents — search, social, retargeting — each needing its own card and its own ceiling. Assignment scopes a card to exactly one agent: only the assigned agent can pull the credentials.
// Bind this card to one buyer agent.
await fetch(`https://api.usenaive.ai/v1/cards/${card.id}/assign`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NAIVE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ agent_id: searchBuyerAgentId }),
});When the assigned agent is ready to place a buy, it pulls the full card credentials at the moment of the call — the PAN lives in memory only for the request, the same way you'd reveal any short-lived secret:
const details = await fetch(
`https://api.usenaive.ai/v1/cards/${card.id}/details`,
{ headers: { Authorization: `Bearer ${process.env.NAIVE_API_KEY}` } },
).then((r) => r.json());
// details.number / details.cvc / details.exp_month / details.exp_year
// details.spent_cents / details.remaining_cents ← the live ceiling
// Hand these to the ad network's billing form. The card works anywhere Visa is accepted.Unassigning is instant revocation — remove the agent and it can no longer reach the card:
await fetch(
`https://api.usenaive.ai/v1/cards/${card.id}/assign/${searchBuyerAgentId}`,
{ method: "DELETE", headers: { Authorization: `Bearer ${process.env.NAIVE_API_KEY}` } },
);Step 5: Log every charge for reconciliation
The card ceiling stops overspend; the log is how you prove and reconcile spend. After every purchase, the agent records a structured transaction next to the raw network charge:
await fetch(`https://api.usenaive.ai/v1/cards/${card.id}/log-transaction`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NAIVE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
amount_cents: 48000,
merchant_name: "Google Ads",
description: "Acme Q3 search campaign — top-of-funnel",
agent_id: searchBuyerAgentId,
}),
});Every card operation also emits an activity event scoped to the tenant user. Pull one client's spend history — there is no way to accidentally read another tenant's events (docs):
const { events } = await acmeClient.logs.query({ limit: 50 });
// Acme's card issuance, top-ups, and charges only — your per-client P&L, for free.Step 6: The four walls around autonomous spend
Here is the part you can't get from "a corporate card in an env var." Four independent controls bound every dollar, and they run inside Naïve, at execution time — not as a check you remembered to write.
Wall 1 — the kit gate. Spend authority is a plan feature. Put a client on a kit with cards disabled and every card call fails server-side, before a card is ever created:
const suspendedKit = await naive.accountKits.create({
name: "Suspended",
primitives_config: { cards: { enabled: false } },
});
await naive.accountKits.assignUser(suspendedKit.id, acme.id);
try {
await acmeClient.cards.create({ name: "blocked", spendingLimitCents: 10000 });
} catch (err) {
if (err instanceof NaiveError && err.code === "forbidden") {
console.log(err.hint); // "primitive_disabled_by_kit"
}
}Wall 2 — the issuer ceiling. The spending_limit_cents on the card is enforced by the card network at authorization time. A swipe that would exceed the card's remaining_cents is declined at the network — there is no "try again harder" for a misaligned agent. This is the wall the prompt cannot argue past, because your code isn't in the loop; the issuer is.
Wall 3 — assignment. Credentials are scoped to the assigned agent. An agent that isn't assigned to a card can't pull its PAN, and unassigning (Step 4) revokes access instantly. One compromised buyer agent can't reach another agent's card.
Wall 4 — the approval gate. Minting a card or raising its ceiling is approval-gated. Topping up goes through the same isPendingApproval path as issuance:
// Raising a client's budget is a human decision, not an agent decision.
const topup = await fetch(`https://api.usenaive.ai/v1/cards/${card.id}/top-up`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NAIVE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ amount_cents: 100000 }),
}).then((r) => r.json());
// Returns a checkout_url for the top-up payment — a human funds the raise.The four compose:
- Kit gate → spend authority toggled per client with one config edit, applied on the next call.
- Issuer ceiling → the hard cap, enforced by the network even if the agent is compromised.
- Assignment → the credential is scoped to one agent and revocable instantly.
- Approval gate → minting money and raising caps wait for a human.
Step 7: Why identity is the real unlock
A card that merchants decline is useless to an autonomous buyer. Naïve's cards run on real rails, and that requires a real principal:
- Managed virtual cards (
provider: "managed_virtual") are full virtual Visa/Mastercard with no spending maximum, issued against the KYC cardholder you set up in Step 2. This is what an ad-buying platform runs on. - Prepaid gift cards (
provider: "prepaid_gift", the default) need no cardholder but are capped at $150.00 — good for sandboxing a new agent or a one-off experiment with a hard cap that can't bleed into your runway.
The provider choice is also enforced. Ask for a prepaid card over the cap, or a managed card with no cardholder, and the API rejects it up front (docs):
try {
await acmeClient.cards.create({ name: "too big", spendingLimitCents: 20000 });
// with provider "prepaid_gift": > $150.00
} catch (err) {
if (err instanceof NaiveError && err.code === "invalid_input") {
console.log(err.hint); // prepaid max, or "create a cardholder first"
}
}That is the difference between "we let agents spend" and "an autonomous agent holds a network-accepted card, bound to a verified cardholder, with an issuer-enforced spending ceiling."
What you didn't build
Count the infrastructure you skipped — the part that is normally the whole product:
- A card-issuing ledger and network integration — managed virtual cards on the Visa/Mastercard rails, funded through hosted checkout. (Cards.)
- A KYC cardholder of record so cards aren't declined at real merchants. (Cardholder +
/kyc.) - A network-enforced spend ceiling per card, declined at authorization — not a check in your code. (
spending_limit_cents.) - Per-agent credential scoping with instant revocation. (Assignment.)
- A human-in-the-loop gate on issuing cards and raising caps. (Approvals.)
- A per-client audit log of every charge, ready for reconciliation. (Logs +
log-transaction.)
You wrote the media-buying logic. Naïve enforced who the cardholder is, how much each agent can spend, which agent holds the card, and what needs a human before funds move — at execution time, server-side, per client.
Ship it
- Model your clients as tenant users and give them an Ad Buyer Account Kit with
cardsenabled andrequiresApprovalon. - Set up the company cardholder once, then issue a managed virtual card per agent with a
spendingLimitCentsceiling. - Assign each card to one buyer agent; pull the PAN at execution time and revoke by unassigning.
- Trust the card network to enforce the ceiling and the kit to gate the primitive — not your prompt.
Start from the Cards guide, the Account Kits guide, and the Approvals guide. Get a workspace key in Naïve Studio and give your agents money they can't misspend.
Why can't I just give my agent a corporate card number in an environment variable?+
What actually stops the agent from overspending?+
Do I need KYC to issue cards to agents?+
How does a card get scoped to a single agent?+
How is issuance approval-gated, and what does the agent see?+
How do I keep a clean audit trail of agent spend?+
Building the autonomous company infrastructure.
Spend caps, human approvals, and instant revoke for AI agents must be enforced server-side — Naïve's governance gateway applies all three on every call.
How to add human approval to an AI agent: gate sensitive actions on your Account Kit, surface pending approvals, and replay on approve. A step-by-step guide.
Issue virtual cards for your agents — a prepaid gift card or a managed virtual card — funded via checkout, capped by a spending limit, and fully audited.
Human-in-the-loop approvals that freeze sensitive actions until you say yes, a per-user audit trail of everything an agent did, short-lived scoped MCP sessions, and unified async job tracking — the controls that make an autonomous fleet safe to run.