paybondpaybond
Sign in

Agents SDK tutorial (Python)

Create an intent, guard OpenAI Agents SDK tool execution with Paybond, and submit signed evidence from Python.

Agents SDK tutorial (Python)

This path uses the first-party Python integration in paybond_kit.agents_sdk. The same binding and guardrail that appear below can plug into a real Runner.run(...) flow; the example here executes the guard directly so you can rehearse the full Paybond lifecycle without adding model wiring first.

What you will build

One guarded tool flow:

  1. Open a tenant-bound Paybond session.
  2. Create an intent for travel.book_hotel.
  3. Read the returned capability_token.
  4. Run the OpenAI Agents input guardrail against the tool call.
  5. Execute the tool only if Harbor allows it.
  6. Submit signed evidence and inspect the resulting Harbor state.

Install

For published packages:

pip install "paybond-kit[agents]"

For the repo example:

cd examples/paybond-kit-openai-agents-python
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

Required environment

export PAYBOND_GATEWAY_URL="https://gateway.example.com"
export PAYBOND_HARBOR_URL="https://harbor.example.com"
export PAYBOND_API_KEY="paybond_sk_..."
export PAYBOND_PRINCIPAL_DID="did:web:example.com#principal"
export PAYBOND_PRINCIPAL_SEED_HEX="..."
export PAYBOND_PAYEE_DID="did:web:example.com#hotel-booker"
export PAYBOND_PAYEE_SEED_HEX="..."

End-to-end flow

import asyncio
import os
from types import SimpleNamespace
from uuid import UUID, uuid4

from agents.tool_context import ToolContext
from agents.tool_guardrails import ToolInputGuardrailData
from paybond_kit import Paybond
from paybond_kit.agents_sdk import (
    PaybondCapabilityBinding,
    paybond_capability_input_guardrail,
)


def seed32_from_hex(env_name: str) -> bytes:
    raw = bytes.fromhex(os.environ[env_name])
    if len(raw) != 32:
        raise ValueError(f"{env_name} must decode to exactly 32 bytes")
    return raw


async def book_hotel(city: str, nightly_budget_cents: int) -> dict[str, object]:
    return {
        "hotel": "Harbor House",
        "city": city,
        "status": "confirmed",
        "price_cents": nightly_budget_cents,
        "confirmation": "HB-2049",
    }


async def main() -> None:
    paybond = await Paybond.open(
        gateway_base_url=os.environ["PAYBOND_GATEWAY_URL"],
        api_key=os.environ["PAYBOND_API_KEY"],
        harbor_base_url=os.environ["PAYBOND_HARBOR_URL"],
    )
    try:
        intent_id = uuid4()
        created = await paybond.intents.create(
            principal_did=os.environ["PAYBOND_PRINCIPAL_DID"],
            principal_signing_seed=seed32_from_hex("PAYBOND_PRINCIPAL_SEED_HEX"),
            payee_did=os.environ["PAYBOND_PAYEE_DID"],
            budget={"currency": "usd", "max_spend_usd": 200},
            predicate={
                "version": 1,
                "root": {
                    "op": "and",
                    "clauses": [
                        {
                            "op": "completion",
                            "path": ["reservation", "status"],
                            "value": "confirmed",
                        },
                        {
                            "op": "budget_cap",
                            "path": ["reservation", "price_cents"],
                        },
                    ],
                },
            },
            currency="usd",
            amount_cents=20_000,
            evidence_schema={"type": "object", "properties": {"reservation": {"type": "object"}}},
            deadline_rfc3339="2030-12-31T23:59:59Z",
            allowed_tools=["travel.book_hotel"],
            intent_id=intent_id,
            idempotency_key=f"intent:{intent_id}",
        )

        capability_token = str(created.get("capability_token") or "")
        if not capability_token:
            raise RuntimeError("intent created without capability_token; ensure the intent is funded")

        binding = PaybondCapabilityBinding(
            harbor=paybond.harbor,
            intent_id=UUID(str(intent_id)),
            capability_token=capability_token,
        )

        guard = paybond_capability_input_guardrail(requested_spend_cents=18_700)
        ctx = ToolContext(
            binding,
            tool_name="book_hotel",
            tool_call_id="call-1",
            tool_arguments='{"city":"Lisbon","nightly_budget_cents":18700}',
            tool_namespace="travel",
        )
        decision = await guard.run(
            ToolInputGuardrailData(context=ctx, agent=SimpleNamespace(name="hotel-booker"))
        )
        if decision.behavior["type"] != "allow":
            raise RuntimeError(f"tool denied: {decision.behavior}")

        reservation = await book_hotel("Lisbon", 18_700)
        submitted = await paybond.intents.submit_evidence(
            UUID(str(intent_id)),
            {"reservation": reservation},
            payee_did=os.environ["PAYBOND_PAYEE_DID"],
            payee_signing_seed=seed32_from_hex("PAYBOND_PAYEE_SEED_HEX"),
            idempotency_key=f"evidence:{intent_id}",
        )

        print(
            {
                "intent_state": created["state"],
                "guard": decision.behavior["type"],
                "settlement_state": submitted["state"],
                "predicate_passed": submitted.get("predicate_passed"),
            }
        )
    finally:
        await paybond.aclose()


asyncio.run(main())

Where the OpenAI Agents SDK fits

The direct guard.run(...) call above uses the same objects that a real OpenAI Agents run uses:

  • PaybondCapabilityBinding carries (harbor, intent_id, capability_token).
  • paybond_capability_input_guardrail() performs Harbor POST /verify.
  • ToolContext.qualified_tool_name becomes the operation string, so travel.book_hotel must appear in the intent allow-list exactly.

When you wire this into a live agent run, pass context=PaybondCapabilityBinding(...) into Runner.run(...) and attach the guardrail to the tool definition.

Expected Harbor outcome

  • create returns capability_token only after the intent is funded.
  • submit_evidence returns a state plus predicate_passed.
  • Release vs refund confirmation remains an operator-tier action in the current Kit surface. See Harbor API for settlement confirmation.

Common failure cases

  • Missing capability_token: the intent is not funded yet.
  • Guardrail rejection: allowed_tools did not match the tool name, spend exceeded the capability, or the token is invalid.
  • Tenant mismatch: treat as severity-zero; do not retry with another client or tenant id.
  • Evidence rejection: the payee seed does not match the payee DID bound on the intent, or the payload does not satisfy the predicate/schema.

Reference implementation