Loading docs...
ZeroClick lets agents pay for your API without you building agent billing yourself. You list what you sell, route agent traffic through your ZeroClick pay URL, and add a small guard to your API so only ZeroClick-signed requests can reach billable endpoints.
Money goes from the buying agent to your connected Stripe account. ZeroClick handles agent identity, payment challenges, request IDs, and usage records.
Use of ZeroClick is governed by the ZeroClick Seller Terms of Service.
ZeroClick is the payment and access layer in front of your existing API. We manage the MPP and x402 endpoints that AI agents call, pay through, and use to reach your services. You keep your product, your upstream API, your pricing, and your customer experience; ZeroClick turns that into an agent-ready paid endpoint.
Sellers get a hosted pay URL, agent wallet authentication, payment challenges, payment verification, signed proxy requests, request IDs, usage checks, and usage records. You do not need to build a checkout flow for agents, run protocol-specific payment infrastructure, or issue a different API key to every buying agent.
To integrate, you provide the API you already operate, configure services, meters, and plans in the dashboard, connect Stripe, and add a small guard to your billed endpoints. Your server verifies that requests came from ZeroClick, asks ZeroClick whether the requested usage is allowed, returns a standard payment-required response when it is not, and reports usage when work is completed.
Seller is your company or product in ZeroClick. Your seller gets a ZeroClick pay URL like https://acme.pay.zeroclick.io. This is the URL agents call and pay through.
Service is one thing agents buy or use from you. It can be a specific LLM model, a data API, a workflow, or a platform feature, such as gpt-oss-120b, image-search, or product-watch. Usage is reported against a service.
Meter is a billable measurement inside a service, such as requests, output_tokens, product_watch_hours, or search_results.
Plan is how agents pay. A plan can be pay as you go, credit-based, subscription, or subscription with usage. Plans belong to the seller; services and meters describe what was used.
In the dashboard:
usage:read and usage:write scopes.API keys carry one or more scopes:
| Scope | Grants |
|---|---|
admin:read | Read your sellers, services, meters, plans, prices, custom domains, signing-secret metadata, and analytics. |
admin:write | Create, update, and delete that configuration, including creating and revoking signing secrets. It does not include admin:read; grant both for a key that manages configuration. |
usage:read | Check whether requested usage is allowed. |
usage:write | Report billable usage. |
Use admin:* keys for tooling and automation that manages your ZeroClick configuration, and separate usage:* keys for the server integration that checks and reports usage. Keys created before the admin:* scopes existed carried billing:* and secrets:* scopes; they were migrated to the equivalent admin:* scopes with the same access.
After that, agents call your ZeroClick pay host instead of your upstream API:
Agent calls: https://acme.pay.zeroclick.io/v1/say-hello
ZeroClick calls: https://api.acme.com/v1/say-helloYour upstream API should treat every billable endpoint as private to ZeroClick. If a request is missing ZeroClick headers or has an invalid signature, reject it.
Install the web-native seller SDK in your server application:
npm install @zeroclickai/sellersCreate one client for your seller. The keys in signingSecrets are the zcsec_... IDs shown with your signing secrets in the dashboard. Keep the previous secret available while rotating so in-flight requests signed with either kid continue to verify.
import { createSeller } from "@zeroclickai/sellers";
const previousSigningSecret =
process.env.ZEROCLICK_SIGNING_SECRET_PREVIOUS;
export const zeroClick = createSeller({
signingSecrets: {
zcsec_current: process.env.ZEROCLICK_SIGNING_SECRET_CURRENT!,
...(previousSigningSecret
? { zcsec_previous: previousSigningSecret }
: {}),
},
// This key needs both usage:read and usage:write scopes.
apiKey: process.env.ZEROCLICK_API_KEY!,
allowanceUnavailable: "allow",
checkTimeoutMs: 1500,
onAllowanceUnavailable(error) {
console.warn("ZeroClick allowance check unavailable", {
code: error.code,
operation: error.context.operation,
});
},
});The defaults are a five-minute signature tolerance, a 1.5-second allowance timeout, and allowanceUnavailable: "allow". You can omit the explicit values above when those defaults fit your application.
ZeroClick forwards requests to your upstream with these headers:
zc-request-id: zcreq_...
zc-agent-id: agt_... # omitted on an anonymous probe
zc-signature: t=1760000000,kid=zcsec_...,v1=<hex-hmac>Call guard before doing any billable work. It verifies the timestamp and signature against the exact request bytes, then checks the requested allowance with ZeroClick. The SDK reads a clone, so your original request body remains available to the application.
import { zeroClick } from "./zeroclick.ts";
export async function POST(request: Request) {
const serviceSlug = "hello-api";
const usage = [{ meterSlug: "say_hello_requests", quantity: 1 }];
const decision = await zeroClick.guard(request, {
serviceSlug,
usage,
});
if (decision.action === "deny") return decision.response;
const result = { message: "Hello from Acme" };
return zeroClick.withUsage(
Response.json(result),
usage.map((item) => ({ serviceSlug, ...item })),
);
}Use this pattern on every endpoint that requires ZeroClick authentication or billing. A valid anonymous probe also reaches the allow/deny flow: its decision.context.zcAgentId is null until the buyer proves identity. Do not reject a request only because zc-agent-id is absent.
Frameworks with web-native route handlers can pass their Request directly. For Express, Fastify, and other Node-specific request types, adapt the request at the framework boundary before a body parser changes it. The adapted Request must retain the original method, full path and query, headers, and exact body bytes; never verify a re-serialized JSON body.
Some endpoints cost nothing but serve only the calling buyer — reading back work that buyer created, for example. Use guardIdentity for these instead of guard:
export async function GET(request: Request) {
const decision = await zeroClick.guardIdentity(request, {
serviceSlug: "research-api",
});
if (decision.action === "deny") return decision.response;
const job = await findJob(jobId, { owner: decision.context.zcAgentId });
if (!job) return Response.json({ error: "not_found" }, { status: 404 });
return Response.json(job);
}It verifies the signature exactly like guard, then requires a proven buyer instead of an allowance: with zc-agent-id present it allows; when absent it denies with a 402 that ZeroClick answers with a free identity challenge, retrying the request with the buyer's zc-agent-id attached. No allowance call is made and no zc-usage belongs on the response. Identity alone is not billable — reportUsage against a buyer with no active access fails with access_not_found.
guard sends all meters needed for the action in one allowance check. Quantities must be positive integers, and a check must not contain the same meter slug twice. Declare a known amount with quantity, or the most a request could use with maxQuantity (see "Charging up to a maximum" below).
{
"serviceSlug": "product-watch",
"usage": [
{ "meterSlug": "product_watch_hours", "quantity": 2 },
{ "meterSlug": "product_watch_checks", "quantity": 1 }
]
}When the signature is missing, stale, or invalid, the deny decision contains the SDK's 401 response and the allowance API is not called. When a valid request has insufficient allowance, the deny decision contains the exact seller 402 response ZeroClick expects:
{
"error": "payment_required",
"serviceSlug": "product-watch",
"planSlug": "growth",
"usage": [{ "meterSlug": "product_watch_hours", "quantity": 2 }]
}planSlug is optional in the guard input. Include it when the request should be priced against a specific plan, such as overage on a usage-based subscription. If you omit it for pay as you go usage, ZeroClick chooses the lowest priced pay as you go plan that can price the requested meters.
Use the same check for pay as you go, credit, subscriptions, and subscription overage. Your endpoint should simply check, reject with the schema above when needed, and allow the request when the check says it is allowed.
Allowance infrastructure failures happen only after the request signature verifies. Configure their behavior on createSeller:
"allow" is the default. guard returns an allow decision with allowance.status === "unavailable" after the timeout."deny" returns an SDK-provided 503 response."throw" throws a typed ZCError for your application to handle.Use onAllowanceUnavailable for operational logging. Never apply the fail-open policy to a missing or invalid signature.
Sometimes you cannot know how much a request will use until the work is done — an LLM's output tokens, seconds of processing time. For those, declare the most the request could possibly use with maxQuantity instead of quantity, in both the usage check and your 402 payment-required response:
{
"usage": [
{ "meterSlug": "requests", "quantity": 1 },
{ "meterSlug": "output_tokens", "maxQuantity": 16384 }
]
}Each item takes exactly one of the two — an item that sends both quantity and maxQuantity is rejected.
The agent reserves the maximum up front. When you report what was actually used (zc-usage), ZeroClick bills only the real amount. Requests carrying a maxQuantity item are payable only on the reserve-and-pay-actual rails — the x402 upto scheme, or an MPP escrow session where that rail is enabled — which settle the actual amount on-chain while the reserved remainder never leaves the buyer's wallet. That reserve-rails-only design is deliberate: it is what guarantees a buyer's unspent ceiling can never turn into stranded credit. (Buyers who already hold a prepaid balance with you from a plan purchase can still cover capped usage from it, billed at actual.) Two consequences to know before you set a cap: buyers need a client that speaks a reserve rail (or an existing balance with you), and until at least one reserve rail is enabled for your services, capped requests are declined with 503 capped_pricing_unavailable — fixed-price requests are unaffected either way.
Rules of thumb:
quantity are committed charges: they bill as declared whether or not your usage report mentions them.maxQuantity items bill nothing (you chose not to charge) and fixed items still bill.upto buyers are simply never debited, and an MPP escrow is closed at $0 so the full deposit refunds immediately.upto rail, settlement happens just after you deliver — not before. In the rare case a buyer empties their wallet between reserving the ceiling and that settle, the charge for that one request can bounce and you are not paid for it. It is bounded to a single request's actual amount, surfaced in the pending-settlement warnings, and never touches other requests. An MPP escrow session and a prepaid balance are both immune — those funds are locked before you deliver — so prefer escrow, or require a prepaid balance, if you need every delivered request guaranteed paid.The usage check accepts the same shape, so you can ask whether a buyer can afford the maximum before starting open-ended work.
You can also set a per-meter Max usage per request default in the dashboard, on the plan's pricing row. When a usage check or 402 response lists a meter with neither quantity nor maxQuantity, ZeroClick fills in that stored default as the ceiling before pricing. A maxQuantity (or quantity) you send on the item always wins; the default only fills the gap, so you can rely on it as a fallback and still override it per request. If a meter has no default and an item declares neither, the request is not priced.
You declare nothing extra for any of this. Paying the actual amount is the default on crypto rails; the credit-remainder path is the fallback for buyers who prepaid the whole ceiling. Both settle from the same zc-usage report you already send, and the choice is the buyer's — it is invisible to you.
For normal request and response APIs, withUsage reports usage synchronously by returning the validated zc-usage header with your successful response. It does not consume the response body.
return zeroClick.withUsage(
Response.json(result),
[
{
serviceSlug: "product-watch",
meterSlug: "product_watch_hours",
quantity: 2,
},
],
);ZeroClick records the usage and removes the zc-usage header before the response goes back to the agent.
For work that finishes later, use reportUsage. Persist the verified zcAgentId with the job and supply a stable idempotencyKey so retries do not double-record usage. The SDK does not invent idempotency keys or automatically retry billable work.
await zeroClick.reportUsage({
idempotencyKey: `job_${job.id}_final_tokens`,
zcAgentId: job.zcAgentId,
serviceSlug: "research-api",
meterSlug: "output_tokens",
quantity: 4200,
});The SDK validates public inputs and ZeroClick API responses at runtime. Malformed method arguments, transport failures, unsuccessful API responses, and invalid API responses throw ZCError instances. Use isZCError to narrow them without depending on error strings.
import { isZCError } from "@zeroclickai/sellers";
try {
await zeroClick.reportUsage(report);
} catch (error) {
if (isZCError(error, "api_status_error")) {
console.error("ZeroClick rejected the usage report", {
status: error.context.status,
reason: error.context.reason,
});
} else if (isZCError(error)) {
console.error("ZeroClick SDK operation failed", {
code: error.code,
operation: error.context.operation,
});
}
throw error;
}Error context is sanitized and does not include signing secrets, API keys, or request body bytes. Expected guard outcomes remain decisions: return decision.response for a deny instead of treating an invalid signature or allowance denial as an exception.
Connect Stripe in the dashboard before charging agents. ZeroClick creates payment requests for the buying agent and routes settlement to your connected Stripe account.
You do not need to invoice agents, collect cards, or handle wallet credentials in your API. Your server uses @zeroclickai/sellers to verify ZeroClick, check allowance, reject requests that need payment, and report usage.