---
title: Payment Skill
description: Up-front token validation, per-request locking, and final settlement for agent billing.
---

# Payment Skill

Payment processing and billing skill for the Robutler platform. This skill enforces billing policies up-front and finalizes charges when a request completes.

> [!NOTE]
> For full x402 protocol support (HTTP endpoint payments, blockchain payments, automatic exchange), see [PaymentSkillX402](../robutler/payments-x402.md). PaymentSkill focuses on tool-level charging and basic token validation, while PaymentSkillX402 extends it with multi-scheme payments and automatic payment handling for HTTP APIs.

## Key Features
- Payment token validation during `on_connection` (returns 402 if required and missing)
- LLM cost calculation via server-side `MODEL_PRICING` catalog
- Tool pricing via optional `@pricing` decorator (results logged to `context.usage` by the agent)
- Final charging based on `context.usage` at `finalize_connection`
- Optional async/sync `amount_calculator` to customize total charge
- Transaction creation via Portal API
- Depends on `AuthSkill` for user identity propagation

## Configuration
- `enable_billing` (default: true)
- `agent_pricing_percent` (percent, e.g., `20` for 20%)
- `minimum_balance` (USD required to proceed; 0 allows free trials without up-front token)
- `robutler_api_url`, `robutler_api_key` (server-to-portal calls)
- `amount_calculator` (optional): async or sync callable `(llm_cost_usd, tool_cost_usd, agent_pricing_percent_percent) -> float`
  - Default: `(llm + tool) * (1 + agent_pricing_percent_percent/100)`

## Example: Add Payment Skill to an Agent

```typescript tab="TypeScript"
import { BaseAgent } from 'webagents';
import { AuthSkill } from 'webagents/skills/auth';
import { PaymentSkill } from 'webagents/skills/payments';

const agent = new BaseAgent({
  name: 'paid-agent',
  model: 'openai/gpt-4o',
  skills: [
    new AuthSkill(),
    new PaymentSkill({
      enableBilling: true,
      minimumBalance: 1.0,
      agentFee: 0.05,
    }),
  ],
});
```

```python tab="Python"
from webagents.agents import BaseAgent
from webagents.agents.skills.robutler.auth.skill import AuthSkill
from webagents.agents.skills.robutler.payments import PaymentSkill

agent = BaseAgent(
    name="paid-agent",
    model="openai/gpt-4o",
    skills={
        "auth": AuthSkill(),
        "payments": PaymentSkill({
            "enable_billing": True,
            "agent_pricing_percent": 20,
            "minimum_balance": 1.0,
        })
    },
)
```

## Tool Pricing with @pricing Decorator (optional)

The PaymentSkill provides a `@pricing` decorator to annotate tools with pricing metadata. Tools can also return
explicit usage objects and will be accounted from `context.usage` during finalize.

```typescript tab="TypeScript"
import { Skill, tool, pricing } from 'webagents';

class BillingSkill extends Skill {
  readonly name = 'billing';

  @tool({ description: 'Query database — costs 0.05 credits per call' })
  @pricing({ creditsPerCall: 0.05, reason: 'Database query' })
  async queryDatabase(params: { sql: string }): Promise<{ results: unknown[] }> {
    return { results: [] };
  }

  @tool({ description: 'Analyze data with variable pricing based on complexity' })
  @pricing()
  async analyzeData(params: { data: string }): Promise<unknown> {
    const complexity = params.data.length;
    const credits = Math.max(0.01, complexity * 0.001);
    return {
      result: `Analysis of ${complexity} characters`,
      _pricing: {
        credits,
        reason: `Data analysis of ${complexity} chars`,
        metadata: { characterCount: complexity, ratePerChar: 0.001 },
      },
    };
  }
}
```

```python tab="Python"
from webagents import tool
from webagents.agents.skills.robutler.payments import pricing, PricingInfo

@tool
@pricing(credits_per_call=0.05, reason="Database query")
async def query_database(sql: str) -> dict:
    """Query database - costs 0.05 credits per call"""
    return {"results": [...]}

@tool
@pricing()
async def analyze_data(data: str) -> tuple:
    """Analyze data with variable pricing based on complexity"""
    complexity = len(data)
    result = f"Analysis of {complexity} characters"

    credits = max(0.01, complexity * 0.001)

    pricing_info = PricingInfo(
        credits=credits,
        reason=f"Data analysis of {complexity} chars",
        metadata={"character_count": complexity, "rate_per_char": 0.001}
    )
    return result, pricing_info
```

### Pricing Options

1. **Fixed Pricing**: `@pricing(credits_per_call=0.05)` (0.05 credits per call)
2. **Dynamic Pricing**: Return `(result, PricingInfo(credits=0.15, ...))`
3. **Conditional Pricing**: Override base pricing in function logic

### Cost Calculation

- **LLM Costs**: Computed from `_llm_usage` context (input/output tokens × model pricing rates). Skipped when `is_byok: true`.
- **Tool Costs**: Read from tool billing metadata (`_billing` on tool results), validated and capped by PaymentSkill's `after_tool` hook.
- **Total**: If `amount_calculator` is provided, its return value is used; otherwise `(llm + tool) * (1 + agent_pricing_percent_percent/100)`

## Example: Validate a Payment Token

```typescript tab="TypeScript"
import { Skill, tool } from 'webagents';
import type { PaymentSkill } from 'webagents/skills/payments';

class PaymentOpsSkill extends Skill {
  readonly name = 'payment-ops';

  @tool({ description: 'Validate a payment token' })
  async validateToken(params: { token: string }): Promise<string> {
    const payments = this.agent!.skills.find(
      (s) => s.name === 'payments',
    ) as PaymentSkill;
    const result = await payments.validatePaymentToken(params.token);
    return JSON.stringify(result);
  }
}
```

```python tab="Python"
from webagents.agents.skills import Skill, tool

class PaymentOpsSkill(Skill):
    def __init__(self):
        super().__init__()
        self.payment = self.agent.skills["payment"]

    @tool
    async def validate_token(self, token: str) -> str:
        """Validate a payment token"""
        result = await self.payment.validate_payment_token(token)
        return str(result)
```

## Hook Integration

The PaymentSkill uses UAMP lifecycle hooks for billing at every stage of a request:

- **`on_connection`**: Validate payment token and check balance. If `enable_billing` and no token is provided while `minimum_balance > 0`, a 402 error is raised and processing stops.
- **`before_llm_call`**: Reads `_llm_capabilities` from context (set by the LLM skill) to estimate maximum LLM cost and lock funds. The lock covers worst-case output at the model's pricing rates.
- **`after_llm_call`**: Reads `_llm_usage` from context (set by the LLM skill after streaming completes). If `is_byok: true`, skips LLM billing (the user paid their provider directly). Otherwise, calculates actual LLM cost and records it for settlement.
- **`before_tool` / `after_tool`**: Validates tool pricing metadata and caps billed amounts. The `charge_type` is validated against a fixed enum and `actualCost` is capped at 10x the configured `perCall` price to prevent malicious tools from inflating charges.
- **`finalize_connection`**: Aggregate all LLM and tool costs, compute final amount with agent markup, and settle the payment token.

### BYOK Billing Behavior

| Scenario | LLM Billing | Tool Billing | Agent Markup |
|----------|-------------|--------------|--------------|
| Platform key (default) | Charged | Charged | Applied to LLM + tools |
| BYOK (`is_byok: true`) | Skipped | Charged | Applied to tools only |
| Agent developer's key | Charged (agent markup covers cost) | Charged | Applied to LLM + tools |

Tool fees are **always** billed regardless of the key scenario. BYOK only exempts LLM inference costs.

## Context Namespacing

The PaymentSkill stores data in the `payments` namespace of the request context:

```typescript tab="TypeScript"
import { getContext } from 'webagents';

const context = getContext();
const payments = context.payments;
const paymentToken = payments?.paymentToken;
```

```python tab="Python"
from webagents.server.context.context_vars import get_context

context = get_context()
payments_data = getattr(context, 'payments', None)
payment_token = getattr(payments_data, 'payment_token', None) if payments_data else None
```

## Usage Tracking

All usage is centralized on `context.usage` by the agent:

- LLM usage records are appended after each completion (including streaming final usage chunk).
- Tool usage is appended when a priced tool returns `(result, usage_payload)`; the agent unwraps the result and stores `usage_payload` as a `{type: 'tool', pricing: {...}}` record.

At `finalize_connection`, the Payment Skill sums LLM and tool costs from `context.usage` and performs the charge.

## Advanced: amount_calculator

You can provide an async or sync `amount_calculator` to fully control the final charge amount:

```typescript tab="TypeScript"
// Coming soon — track at https://github.com/robutlerai/webagents/issues
// In TypeScript, use `agentFee` (fixed) and `creditsPerToken` (per-token
// override) on PaymentSkillConfig instead of an `amount_calculator`.
// Track parity at ../../internal/python-typescript-parity.md.
```

```python tab="Python"
async def my_amount_calculator(
    llm_cost_usd: float,
    tool_cost_usd: float,
    agent_pricing_percent_percent: float,
) -> float:
    base = llm_cost_usd + tool_cost_usd
    return base * (1 + agent_pricing_percent_percent / 100)

payment = PaymentSkill({
    "enable_billing": True,
    "agent_pricing_percent": 15,
    "amount_calculator": my_amount_calculator,
})
```

If omitted, the default formula is used: `(llm + tool) * (1 + agent_pricing_percent/100)`.

## Dependencies

- **AuthSkill**: Required for user identity headers (`X-Origin-User-ID`, `X-Peer-User-ID`, `X-Agent-Owner-User-ID`). The Payment Skill reads them from the auth namespace on the context.

Implementation: [`typescript/src/skills/payments/skill.ts`](https://github.com/robutlerai/webagents/blob/main/typescript/src/skills/payments/skill.ts) and [`python/webagents/agents/skills/robutler/payments/skill.py`](https://github.com/robutlerai/webagents/blob/main/python/webagents/agents/skills/robutler/payments/skill.py).

## Error semantics (402)

- Missing token while `enable_billing` and `minimum_balance > 0` ➜ 402 Payment Required
- Invalid or expired token ➜ 402 Payment Token Invalid
- Insufficient balance ➜ 402 Insufficient Balance

Finalize hooks still run for cleanup but perform no charge if no token/usage is present.

## Transport-Agnostic Payments

Starting with V2.0, PaymentSkill extracts the payment token in a **transport-agnostic** manner.
The skill reads `context.payment_token` first (set by any transport), then falls back to HTTP
headers (`X-Payment-Token`, `X-PAYMENT`) and query parameters as a legacy path.

This means payment works identically over HTTP Completions, UAMP WebSocket, A2A, ACP, and
Realtime transports -- the transport is responsible for negotiating the token (e.g. via
`payment.required` / `payment.submit` events over UAMP, or a 402 response over HTTP), and the
payment skill only validates and charges.

### Token extraction priority

1. **`context.payment_token`** -- set by the transport (UAMP `session.update`, portal `payment.submit`, etc.)
2. **HTTP header** -- `X-Payment-Token` or `X-PAYMENT` (Completions, A2A)
3. **Query parameter** -- `?payment_token=...` (legacy)

### PaymentTokenRequiredError

When billing is enabled and no token is found, the skill raises `PaymentTokenRequiredError`
(HTTP status 402). Each transport catches this and maps it to its protocol:

| Transport | Behavior |
|-----------|----------|
| **Completions** | Returns 402 JSON before streaming (pre-flight check) |
| **UAMP** | Sends `payment.required` event, waits for `payment.submit`, retries, sends `payment.accepted` |
| **A2A** | Returns `task.failed` with `code: "payment_required"` and `accepts` array |
| **ACP** | Returns JSON-RPC error `-32402` with payment data |
| **Realtime** | Sends `payment.required` event over audio WebSocket |

### Example: UAMP inline payment negotiation

```
Client                      Agent (UAMP)
  │                             │
  ├─ input.text ───────────────►│
  │                             ├─ (skill raises PaymentTokenRequiredError)
  │◄── payment.required ───────┤
  │                             │
  ├─ payment.submit ───────────►│  (token from facilitator)
  │                             ├─ (retry with context.payment_token)
  │◄── response.delta ─────────┤
  │◄── response.done ──────────┤
  │◄── payment.accepted ───────┤
```