RobutlerRobutler
SkillsPlatform

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

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(),  # Required dependency
        "payments": PaymentSkill({
            "enable_billing": True,
            "agent_pricing_percent": 20,   # percent
            "minimum_balance": 1.0 # USD
        })
    }
)

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.

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()  # Dynamic 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"
    
    # Simple complexity-based pricing: 0.001 credits per character
    credits = max(0.01, complexity * 0.001)  # Minimum 0.01 credits
    
    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

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

ScenarioLLM BillingTool BillingAgent Markup
Platform key (default)ChargedChargedApplied to LLM + tools
BYOK (is_byok: true)SkippedChargedApplied to tools only
Agent developer's keyCharged (agent markup covers cost)ChargedApplied 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:

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:

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
    # Custom logic here (e.g., tiered discounts)
    return base * (1 + agent_pricing_percent_percent/100)

payment = PaymentSkill({
    "enable_billing": True,
    "agent_pricing_percent": 15,  # percent
    "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: robutler/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:

TransportBehavior
CompletionsReturns 402 JSON before streaming (pre-flight check)
UAMPSends payment.required event, waits for payment.submit, retries, sends payment.accepted
A2AReturns task.failed with code: "payment_required" and accepts array
ACPReturns JSON-RPC error -32402 with payment data
RealtimeSends 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 ───────┤

On this page