Part 1/4: Building Agents That Help Customers Purchase Online
Part 1 of a 4-part series on agentic payments: an agent buys something for your customer, backed by a double-entry ledger like Blnk to keep a clear audit trail of every financial activity the agent triggers in your system.
AI agents have quietly become part of everyday life. We use them to write, code, research, and generate images to name a few. It was only a matter of time before we started asking them to spend money for us, for example, "Find me a plane ticket to London for $1,000 USD."
Simple request. But behind it is a question most people don't think about: who's actually paying?
Payments require ownership by design. Every balance has an owner, every charge has someone who authorized it, and someone who answers for it when it goes wrong. That someone is always a human.
No matter how intelligent agents become, this doesn't change. An agent can't hold a balance, can't be liable, and can't bear a cost. So when an agent books the wrong flight at 2 AM or renews a subscription your user meant to cancel, the agent doesn't pay for the mistake. Your user does. And your user will blame your product for it.
If you're building a product where agents spend on behalf of users, the real question was never "how do I make agents pay for me?" It's "how do I give my users control over what their agents pay for?" And just as importantly, how do you build that control directly into the product?
That's what this article is about. We'll walk through how to think about payments for AI agents: how to model balances, limits, and conditions so your users can safely delegate spending to agents without ever giving up ownership or control.
The mental model: delegated spending
To make this concrete, let's build for one product: PayPilot, a wallet app with AI capabilities. Users keep money in PayPilot, and they can ask the PayPilot agent to do things with their money like our example earlier: "Find me a plane ticket to London for $1,000 USD."
If the user were doing this themselves, everything would happen at once: they'd see "$860 to British Airways" on a checkout screen and tap Pay. Intent ("wanting to buy") and authorization (actually paying) happen in one single event.
However, with an agent, that single event splits into three:
- User shows intent: "Find me a plane ticket to London for $1,000 USD."
- User authorizes ahead: funds an account for the agent and sets the policy.
- Agent creates a payment intent: it finds the ticket and asks to pay for it. If the intent matches the authorization, it becomes a payment; if it doesn't, it's rejected.
This is delegated spending: the user gives the agent capability, not a specific amount. They stop approving payments one by one and instead define what an acceptable payment looks like per task.
In practice, this workflow has two parts: where the money lives, and what rules define what acceptable means.
Giving the agent an account
The first boundary is where the money lives. An agent should never spend directly from the user's main balance. If it does, every payment intent it generates has access to everything the user owns, and one misunderstood instruction or prompt injection can empty the account.
Instead, give the agent its own account, funded from the user's balance.
Think of the agent as a version of the user inside your system: it acts with the user's authority, but inside its own container. It's the same reason companies hand employees expense cards instead of the company bank login.
In Blnk, the agent's account is just another balance you create, preferably under a separate Agents Ledger. Create one under the user's identity:
const response = await blnk.LedgerBalances.create({
ledger_id: 'agents-ledger-id',
currency: 'USD',
identity_id: 'user-identity-id',
meta_data: {
type: 'agent_account',
agent_task_id: 'task-london-flight-001',
},
}); balance, resp, err := client.LedgerBalance.Create(blnkgo.CreateLedgerBalanceRequest{
LedgerID: "agents-ledger-id",
Currency: "USD",
IdentityID: "user-identity-id",
MetaData: blnkgo.MetaData{
"type": "agent_account",
"agent_task_id": "task-london-flight-001",
},
}) response = blnk.ledger_balances.create({
"ledger_id": "agents-ledger-id",
"currency": "USD",
"identity_id": "user-identity-id",
"meta_data": {
"type": "agent_account",
"agent_task_id": "task-london-flight-001",
},
}) ApiResponse<JsonNode> response = blnk.ledgerBalances().create(
CreateLedgerBalance.create()
.ledgerId("agents-ledger-id")
.currency("USD")
.identityId("user-identity-id")
.metaData(Map.of("type", "agent_account", "agent_task_id", "task-london-flight-001"))); {
"balance_id": "agent-balance-id",
"balance": 0,
"inflight_balance": 0,
"credit_balance": 0,
"debit_balance": 0,
"ledger_id": "ledger-id",
"identity_id": "user-identity-id",
"currency": "USD",
"created_at": "2026-08-24T09:33:35.265582042Z",
"meta_data": {
"type": "agent_account",
"agent_task_id": "task-london-flight-001"
}
} How many balances you create is up to you. You can keep one standing balance per agent for ongoing use, or create a fresh balance per task: fund it with what the task needs, let its rules apply only to that task, and return whatever is left to the user's balance when the task ends.
A standing balance means less setup. A balance per task means tighter control, since each task only ever touches its own money.
For this article, we'll adopt a fresh balance per agent task.
Defining the spending policy
The account caps how much the agent can access. The spending policy decides how it's allowed to spend. This is what defines agentic payments: every payment intent an agent produces must pass the policy before it becomes a transaction.
Policies come in two shapes:
- Overall policies apply to everything the agent does. Never more than $200 in a single transaction, at most $1,000 a month, USD only. These live at the account level. In fact, the funded amount itself is the simplest overall policy you have: the agent can never spend more than its allocated balance.
- Per-request policies apply to a single task. For this ticket: up to $1,000, only with this airline, and if the transaction isn't completed within 30 minutes, it's voided. These live at the intent level, attached to one specific purchase.
For this article, we'll go with per-request policies. Each intent carries its own rules (how much, for what, and how long it's valid), and every intent must pass them before money moves.
For our London ticket task, that check looks like this:
async function handlePaymentIntent(intent: PaymentIntent) {
const policy = await getPolicyForTask(intent.taskId);
// { maxAmount: 1000, allowedMerchant: "british-airways", expiresIn: "30m" }
if (intent.amount > policy.maxAmount) return reject(intent, "over_limit");
if (intent.merchant !== policy.allowedMerchant) return reject(intent, "wrong_merchant");
// only runs after every check passes
// inflight: voided automatically if the transaction isn't completed in 30m
return createTransaction(intent, { inflight: true, expiresIn: policy.expiresIn });
} The order is the whole point: policy first, transaction second.
Executing a payment
Now, our agent account is ready and we’ve set a policy. Up next is what happens when the agent finds the ticket it’s satisfied with. The task balance always starts empty; money only moves when a payment completes, and it moves in four steps: reserve, pay, commit, settle.
Reserve the policy max amount from the customer's balance
When the task starts, hold the policy's max amount on the customer's balance with an inflight transaction into the task balance. Your ledger now records that your customer has an ongoing agent payment task:
TypeScriptconst response = await blnk.Transactions.create({ amount: 1000, precision: 100, currency: 'USD', reference: 'reserve-task-london-flight-001', source: 'user-main-balance-id', destination: 'agent-balance-id', description: 'Reserve policy max for task', inflight: true, meta_data: { agent_task_id: 'task-london-flight-001', }, });Gotransaction, resp, err := client.Transaction.Create( blnkgo.CreateTransactionRequest{ ParentTransaction: blnkgo.ParentTransaction{ Amount: 1000, Precision: 100, Currency: "USD", Reference: "reserve-task-london-flight-001", Source: "user-main-balance-id", Destination: "agent-balance-id", Description: "Reserve policy max for task", MetaData: blnkgo.MetaData{ "agent_task_id": "task-london-flight-001", }, }, Inflight: true, }, )Pythonresponse = blnk.transactions.create({ "amount": 1000, "precision": 100, "currency": "USD", "reference": "reserve-task-london-flight-001", "source": "user-main-balance-id", "destination": "agent-balance-id", "description": "Reserve policy max for task", "inflight": True, "meta_data": { "agent_task_id": "task-london-flight-001", }, })JavaApiResponse<JsonNode> response = blnk.transactions().create( CreateTransactions.create() .amount(1000) .precision(100) .currency("USD") .reference("reserve-task-london-flight-001") .source("user-main-balance-id") .destination("agent-balance-id") .description("Reserve policy max for task") .inflight(true) .metaData(Map.of("agent_task_id", "task-london-flight-001")));200 OK{ "transaction_id": "txn_reserve-london-flight-001", "amount": 1000, "precision": 100, "precise_amount": 100000, "source": "user-main-balance-id", "destination": "agent-balance-id", "reference": "reserve-task-london-flight-001", "currency": "USD", "status": "INFLIGHT", "inflight": true, "meta_data": { "agent_task_id": "task-london-flight-001" } }The $1,000 is now held on the customer's balance, but it hasn't moved. The customer still owns it, and the agent can never receive more than this.
User balance: inflight balance shows that there is a reserved $1,000 on the main balance for the agent use. Pay against the reservation
When the agent finds the $860 ticket and it passes the policy, create the payment as a second inflight transaction: from the agent balance to the merchant (
@World), with a 30-minute expiry date set, and an overdraft limit equal to the policy max amount:TypeScriptconst response = await blnk.Transactions.create({ amount: 860, precision: 100, currency: 'USD', reference: 'pay-task-london-flight-001', source: 'agent-balance-id', destination: '@WorldUSD', description: 'Pay for London ticket', inflight: true, inflight_expiry_date: '2026-08-24T08:05:00+00:00', allow_overdraft: true, overdraft_limit: 1000, meta_data: { agent_task_id: 'task-london-flight-001', }, });Goexpiry, _ := time.Parse(time.RFC3339, "2026-08-24T08:05:00Z") transaction, resp, err := client.Transaction.Create( blnkgo.CreateTransactionRequest{ ParentTransaction: blnkgo.ParentTransaction{ Amount: 860, Precision: 100, Currency: "USD", Reference: "pay-task-london-flight-001", Source: "agent-balance-id", Destination: "@WorldUSD", Description: "Pay for London ticket", MetaData: blnkgo.MetaData{ "agent_task_id": "task-london-flight-001", }, }, Inflight: true, InflightExpiryDate: &expiry, AllowOverdraft: true, OverdraftLimit: 1000, }, )Pythonresponse = blnk.transactions.create({ "amount": 860, "precision": 100, "currency": "USD", "reference": "pay-task-london-flight-001", "source": "agent-balance-id", "destination": "@WorldUSD", "description": "Pay for London ticket", "inflight": True, "inflight_expiry_date": "2026-08-24T08:05:00+00:00", "allow_overdraft": True, "overdraft_limit": 1000, "meta_data": { "agent_task_id": "task-london-flight-001", }, })JavaApiResponse<JsonNode> response = blnk.transactions().create( CreateTransactions.create() .amount(860) .precision(100) .currency("USD") .reference("pay-task-london-flight-001") .source("agent-balance-id") .destination("@WorldUSD") .description("Pay for London ticket") .inflight(true) .inflightExpiryDate("2026-08-24T08:05:00+00:00") .allowOverdraft(true) .overdraftLimit(1000) .metaData(Map.of("agent_task_id", "task-london-flight-001")));200 OK{ "transaction_id": "txn_pay-london-flight-001", "amount": 860, "precision": 100, "precise_amount": 86000, "source": "agent-balance-id", "destination": "@WorldUSD", "reference": "pay-task-london-flight-001", "currency": "USD", "status": "INFLIGHT", "inflight": true, "inflight_expiry_date": "2026-08-24T08:05:00+00:00", "allow_overdraft": true, "overdraft_limit": 1000, "meta_data": { "agent_task_id": "task-london-flight-001" } }Here's what happens:
This is the agent's actual payment to the merchant. It remains in-flight until the purchased value is received (e.g., the flight ticket). - The task balance holds no funds, so the payment runs as an overdraft for now. That's what
allow_overdraftpermits. - Read that negative as a preview, not a debt. Because the payment is still inflight, nothing has actually moved; -$860 is simply how much of the customer's reservation will settle from Step 1 once the payment completes.
- The limit is set on the transaction itself:
overdraft_limitequals the policy max, so the task balance can never go below -$1,000. If the agent tries to spend more anyway, Blnk automatically rejects the payment outright. That acts as your second guard after the policy check. - The expiry is the completion deadline from our policy. If the payment isn't completed within 30 minutes, Blnk voids it automatically and the cash is released back to the customer.
- The task balance holds no funds, so the payment runs as an overdraft for now. That's what
Commit the payment
When the airline confirms the ticket, commit the payment to release the money to the merchant.
The task balance, which started empty, goes to –$860:
TypeScriptconst response = await blnk.Transactions.updateStatus( 'txn_pay-london-flight-001', { status: 'commit' }, );Gotransaction, resp, err := client.Transaction.Update( "txn_pay-london-flight-001", blnkgo.UpdateStatus{ Status: blnkgo.InflightStatusCommit, }, )Pythonresponse = blnk.transactions.update_status( "txn_pay-london-flight-001", { "status": "commit", }, )JavaApiResponse<JsonNode> response = blnk.transactions().updateStatus( "txn_pay-london-flight-001", UpdateTransactionStatus.create().status("commit"));
Agent balance: indicates the portion of the allocated limit the customer is required to settle. Settle the reservation
Next, partially commit the reservation from Step 1 for exactly $860. This settles the user’s obligation to you once the payment as been processed.
The held money finally moves: $860 from the customer's balance into the agent task balance, returning it back to zero:
TypeScriptconst response = await blnk.Transactions.updateStatus( 'txn_reserve-london-flight-001', { status: 'commit', amount: 860, }, );Gotransaction, resp, err := client.Transaction.Update( "txn_reserve-london-flight-001", blnkgo.UpdateStatus{ Status: blnkgo.InflightStatusCommit, Amount: 860, }, )Pythonresponse = blnk.transactions.update_status( "txn_reserve-london-flight-001", { "status": "commit", "amount": 860, }, )JavaApiResponse<JsonNode> response = blnk.transactions().updateStatus( "txn_reserve-london-flight-001", UpdateTransactionStatus.create() .status("commit") .amount(860));
Agent balance: It is now zeroed out; transaction complete and customer payment is settled. The remaining $140 stays held until you void the reservation. Voiding after a partial commit releases only the leftover, so the $140 goes back to the customer:
TypeScriptconst response = await blnk.Transactions.updateStatus( 'txn_reserve-london-flight-001', { status: 'void' }, );Gotransaction, resp, err := client.Transaction.Update( "txn_reserve-london-flight-001", blnkgo.UpdateStatus{ Status: blnkgo.InflightStatusVoid, }, )Pythonresponse = blnk.transactions.update_status( "txn_reserve-london-flight-001", { "status": "void", }, )JavaApiResponse<JsonNode> response = blnk.transactions().updateStatus( "txn_reserve-london-flight-001", UpdateTransactionStatus.create().status("void"));Once the agent task balance is zeroed out, that's it.
User balance: $850 is deducted and the rest is returned back to the user balance. The agent never touched the customer's main balance directly, never spent more than the policy max, and left a complete record of every step in the ledger.
What's possible now
With this foundation in place (capability, an account, a policy, and an execution flow), you can already build a lot more:
- Human approval for edge cases: when an intent is close to passing but not quite, hold it inflight and ask the user to approve it manually.
- Per-merchant policies: allow airlines, block gambling sites, cap food delivery.
- Instant revocation: to fire an agent, move its balance back to the user. No keys to rotate, no cards to cancel.
- Per-task analytics: every task has its own transaction history, so "how much did the agent spend this month?" is a query, not a project.
Want to give this a try? Everything we used here is in our documentation.
