Sell one API call to an agent
If an agent needs to buy access to an API, payment and execution need to be tracked separately. This tutorial walks through a test purchase, checks settlement, and reserves one authorized lookup. Quote a test purchase,
If an agent needs to buy access to an API, payment and execution need to be tracked separately. This tutorial walks through a test purchase, checks settlement, and reserves one authorized lookup.
Quote a test purchase, confirm payment, and reserve one authorized execution.
What you will build
A buyer agent purchases one fixed-price lookup and the seller delivers it after payment settles and the seller's own policy permits it. The examples use TypeScript and Test credentials. Your application supplies durable storage, its authorization policy, and the actual lookup.
You need separate seller and buyer workspaces, a seller test payment account, a 100 USD cents API-call resource, and Test keys with charge:budget, charge:buy, and charge:fulfill for their respective roles. Follow seller and buyer setup first. Keep all keys on your servers and use the same environment throughout.
Install @chitmark/sdk in a TypeScript project. Set CHITMARK_BUYER_BUDGET_KEY, CHITMARK_BUYER_KEY, and CHITMARK_SELLER_FULFILL_KEY to the corresponding Test keys. Set CHITMARK_SELLER_ID, CHITMARK_BUYER_ID, and CHITMARK_RESOURCE_ID to the IDs from setup, and CHITMARK_OPERATION_ID to a new, durable ID for this lookup. CHITMARK_API_BASE_URL is optional and defaults to the production API endpoint. Run the buyer example in your TypeScript runtime; the later snippets are separate steps using the saved state.
Configure a seller and quote a lookup
In the seller workspace, select Test and complete test payment setup. Under Resources, create an enabled API-call resource priced at 100 USD cents for one call. Save its ID and both workspace IDs. Test checkout uses the seller's test payment account.
Use the same server-side values throughout the purchase. Save the lookup inputs, operation ID and digest with the purchase before handing off to checkout. Keep the intent and each mutation's idempotency key stable across retries. A retry uses the same key and exact payload. The examples below use TypeScript. In Python, pass the same request dictionaries to client.charge(...) and read the same response fields.
Set the ceiling and accept
import { createHash } from "node:crypto";
import { Chitmark } from "@chitmark/sdk";
function api(name: string) {
const apiKey = process.env[name];
if (!apiKey?.startsWith("ck_test_")) throw new Error(name + " requires a Test key");
return new Chitmark({ apiKey, baseUrl: process.env.CHITMARK_API_BASE_URL });
}
const owner = api("CHITMARK_BUYER_BUDGET_KEY"); // charge:budget
const buyer = api("CHITMARK_BUYER_KEY"); // charge:buy
const sellerTenantId = process.env.CHITMARK_SELLER_ID!;
const buyerTenantId = process.env.CHITMARK_BUYER_ID!;
const resourceId = process.env.CHITMARK_RESOURCE_ID!;
// Assign and persist this ID in your application before the first request.
const operationId = process.env.CHITMARK_OPERATION_ID!;
const lookup = "acme";
const operationDigest = createHash("sha256")
.update(
JSON.stringify([operationId, sellerTenantId, buyerTenantId, resourceId, lookup]),
)
.digest("hex");
await owner.charge({
operation: "budget",
sellerTenantId,
buyerTenantId,
limit: 500,
idempotencyKey: "budget:" + operationId,
});
const quoted = await buyer.charge({
operation: "quote",
sellerTenantId,
resourceId,
quantity: 1,
operationDigest,
idempotencyKey: "quote:" + operationId,
});
if (!("purchase" in quoted)) throw new Error("Quote unavailable");
const { purchaseId, quote } = quoted.purchase;
if (quote.amount !== 100 || quote.currency !== "usd") throw new Error("Unexpected terms");
const accepted = await buyer.charge({
operation: "accept",
sellerTenantId,
purchaseId,
quoteVersion: quote.version,
maxAmount: quote.amount,
currency: quote.currency,
idempotencyKey: "accept:" + purchaseId,
});
if (!("purchase" in accepted)) throw new Error("Purchase unavailable");
// Save the purchase ID and digest in your application.
console.log({ purchaseId, operationDigest, next: accepted.next });
if (accepted.next === "pay" && accepted.purchase.checkoutUrl) {
// Test script only. In your app, pass the URL to the authorized payer.
console.log(accepted.purchase.checkoutUrl);
}
Complete test checkout and check capture
Open the returned purchase.checkoutUrl for the authorized test payer and complete checkout. If next is wait, read status for the same purchase instead of starting another. Use status to recover the current checkout URL or payment state after an interruption.
After checkout, run the status request with the same buyer key and purchase ID. Keep waiting while payment is pending or authorized. Continue only when payment.state is settled. A checkout redirect or a Trust Decisions allow carries no payment authorization.
Check payment
const status = await buyer.charge({ operation: "status", sellerTenantId, purchaseId });
if (!("purchase" in status)) throw new Error("Purchase unavailable");
if (status.next === "pay" && status.purchase.checkoutUrl) {
console.log(status.purchase.checkoutUrl); // Test script only
}
if (status.purchase.payment.state !== "settled") {
throw new Error("Hold fulfillment until payment is settled");
}
Authorize, reserve, and fulfill one lookup
Run fulfillment on the seller backend using CHITMARK_SELLER_FULFILL_KEY. Load the saved purchase and operation digest. Check that the authenticated buyer may access this lookup under your own policy. If a Trust Decisions verdict justified this specific permission, include its stored eventId when you record permission.
Persist a unique execution ID before requesting the reservation. The example uses an application-provided deliverOnce function for the actual lookup. Implement it so the output or business effect and the execution ID commit together. Reuse the ID on retry. If you lose the reserve response, read the current purchase and reconcile instead of executing without a fresh granted lease.
Reserve and report usage
const seller = api("CHITMARK_SELLER_FULFILL_KEY"); // charge:fulfill
// Application values loaded from durable state after checking local policy.
if (!localPermission) throw new Error("Lookup not authorized");
await seller.charge({
operation: "permission",
sellerTenantId,
purchaseId,
allowed: true,
idempotencyKey: "permission:" + executionId,
});
const reserved = await seller.charge({
operation: "reserve",
sellerTenantId,
purchaseId,
executionId,
operationDigest,
units: 1,
idempotencyKey: "reserve:" + executionId,
});
if (
!("purchase" in reserved) ||
reserved.purchase.payment.state !== "settled" ||
!reserved.purchase.permission.allowed ||
!reserved.reservation?.granted ||
reserved.reservation.executionId !== executionId ||
Date.now() >= reserved.reservation.leaseUntil
) {
throw new Error("Hold delivery");
}
await deliverOnce({
purchaseId,
executionId,
lookup,
leaseUntil: reserved.reservation.leaseUntil,
});
await seller.charge({
operation: "usage",
sellerTenantId,
purchaseId,
executionId,
sequence: 1,
usedUnits: 1,
result: "delivered",
idempotencyKey: "usage:" + executionId + ":1",
});
Verify and recover
Read the purchase with the buyer key or find it under Agent Paid Access → Purchases in the seller workspace. Confirm the execution is delivered and has one used unit. Try fulfilling before settlement and with local permission denied: both must hold delivery.
Persist the operation ID, inputs, digest, purchase ID, and execution ID before their respective steps. Retry a lost request with the same idempotency key and exact payload. If the reservation response is lost, inspect and reconcile the purchase instead of delivering without a fresh granted lease. If delivery is uncertain, keep the units reserved and reconcile before retrying. If usage reporting fails after delivery, retry the same usage payload.
The fulfillment example is a server-side integration fragment: implement localPermission and deliverOnce in your application. deliverOnce must durably deduplicate by execution ID and enforce the lease at the execution boundary. Refunds and disputes can change payment state after settlement. See the charge reference for operations and scopes, and the quickstart for live setup.
Read the full guide on Chitmark.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.