8 min read

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.

Part 1/4: Building agents that help customers purchase online

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:

  1. User shows intent: "Find me a plane ticket to London for $1,000 USD."
  2. User authorizes ahead: funds an account for the agent and sets the policy.
  3. 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.
A simple diagram to show what delegated spending is: user finds what they want to buy (intent), authorizes a policy, and then the agent executes based on that policy.
User authorizes a limit; agent can only spend for that limit. When things go wrong, user's main funds remain protected.

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.

Screenshot of Blnk Cloud showing a new agent balance
Details page of a new agent 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:

TypeScript
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',
  },
});
Go
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",
  },
})
Python
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",
  },
})
Java
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")));
200 OK
{
  "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:

spendingPolicy.ts
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.

  1. 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:

    TypeScript
    const 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',
      },
    });
    Go
    transaction, 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,
      },
    )
    Python
    response = 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",
      },
    })
    Java
    ApiResponse<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.

    Screenshot of the Blnk Cloud balance details page showing a balance with $5,000 with a $1,000 inflight debit hold
    User balance: inflight balance shows that there is a reserved $1,000 on the main balance for the agent use.
  2. 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:

    TypeScript
    const 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',
      },
    });
    Go
    expiry, _ := 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,
      },
    )
    Python
    response = 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",
      },
    })
    Java
    ApiResponse<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:

    Screenshot of the transaction details on Blnk Cloud showing an inflight $850 transaction from the agent balance to the merchants
    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_overdraft permits.
    • 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_limit equals 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.
  3. 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:

    TypeScript
    const response = await blnk.Transactions.updateStatus(
      'txn_pay-london-flight-001',
      { status: 'commit' },
    );
    Go
    transaction, resp, err := client.Transaction.Update(
      "txn_pay-london-flight-001",
      blnkgo.UpdateStatus{
        Status: blnkgo.InflightStatusCommit,
      },
    )
    Python
    response = blnk.transactions.update_status(
      "txn_pay-london-flight-001",
      {
        "status": "commit",
      },
    )
    Java
    ApiResponse<JsonNode> response = blnk.transactions().updateStatus(
      "txn_pay-london-flight-001",
      UpdateTransactionStatus.create().status("commit"));
    Screenshot: Agent balance reflects a negative balance following commitment, alongside the now-available pending limit.
    Agent balance: indicates the portion of the allocated limit the customer is required to settle.
  4. 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:

    TypeScript
    const response = await blnk.Transactions.updateStatus(
      'txn_reserve-london-flight-001',
      {
        status: 'commit',
        amount: 860,
      },
    );
    Go
    transaction, resp, err := client.Transaction.Update(
      "txn_reserve-london-flight-001",
      blnkgo.UpdateStatus{
        Status: blnkgo.InflightStatusCommit,
        Amount: 860,
      },
    )
    Python
    response = blnk.transactions.update_status(
      "txn_reserve-london-flight-001",
      {
        "status": "commit",
        "amount": 860,
      },
    )
    Java
    ApiResponse<JsonNode> response = blnk.transactions().updateStatus(
      "txn_reserve-london-flight-001",
      UpdateTransactionStatus.create()
        .status("commit")
        .amount(860));
    Screenshot: Agent balance is now zeroed out, all obligations are settled.
    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:

    TypeScript
    const response = await blnk.Transactions.updateStatus(
      'txn_reserve-london-flight-001',
      { status: 'void' },
    );
    Go
    transaction, resp, err := client.Transaction.Update(
      "txn_reserve-london-flight-001",
      blnkgo.UpdateStatus{
        Status: blnkgo.InflightStatusVoid,
      },
    )
    Python
    response = blnk.transactions.update_status(
      "txn_reserve-london-flight-001",
      {
        "status": "void",
      },
    )
    Java
    ApiResponse<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.

    Screenshot: User balance shows the net balance after the task is done.
    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.

Build agentic payments with inflight transactions

Build agentic payments with inflight transactions

Hold funds until checks pass, then commit or void — the building block for agentic payments on Blnk.

docs.blnkfinance.com

Related articles