paybondpaybond
Sign in

Agent runtime tutorial (Python)

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

This path uses the runtime-neutral adapter in paybond_kit.agent_adapters. The same binding and adapter shown below can plug into provider SDKs, local-model runtimes, queues, and custom orchestrators that expose an application-owned tool executor.

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 Paybond runtime-neutral adapter against the tool call.
  5. Execute the tool only if Harbor allows it.
  6. Submit signed evidence and inspect the resulting Harbor state.

Install

pip install paybond-kit

For a complete example application, see the reference implementation linked at the end of this page.

Required environment

Only PAYBOND_API_KEY comes from Paybond. The APP_* variables are example-local placeholders for values your app or trusted signer owns.

paybond-kit-login
export APP_PRINCIPAL_DID="did:web:example.com#principal"
export APP_PRINCIPAL_SEED_HEX="..."
export APP_PAYEE_DID="did:web:example.com#hotel-booker"
export APP_PAYEE_SEED_HEX="..."
export APP_INTENT_CREATE_RECOGNITION_PROOF_JSON='{"key_id":"..."}'
export APP_EVIDENCE_RECOGNITION_PROOF_JSON='{"key_id":"..."}'
export APP_SETTLEMENT_RAIL="x402_usdc_base" # optional
export APP_X402_PAYMENT_SIGNATURE="demo-signature" # optional

End-to-end flow

import asyncio
import json
import os
from uuid import UUID, uuid4

from paybond_kit import (
    Paybond,
    paybond_runtime_tool_call_adapter,
)


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


def required_json_env(env_name: str) -> dict[str, object]:
    value = json.loads(os.environ[env_name])
    if not isinstance(value, dict):
        raise ValueError(f"{env_name} must contain a JSON object")
    return value


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(
        api_key=os.environ["PAYBOND_API_KEY"],
        expected_environment="sandbox",
    )
    try:
        intent_id = uuid4()
        created = await paybond.intents.create(
            principal_did=os.environ["APP_PRINCIPAL_DID"],
            principal_signing_seed=seed32_from_hex("APP_PRINCIPAL_SEED_HEX"),
            payee_did=os.environ["APP_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"],
            recognition_proof=required_json_env("APP_INTENT_CREATE_RECOGNITION_PROOF_JSON"),
            settlement_rail=(
                "x402_usdc_base"
                if os.environ.get("APP_SETTLEMENT_RAIL") == "x402_usdc_base"
                else "stripe_connect"
            ),
            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")

        guard = paybond.spend_guard(UUID(str(intent_id)), capability_token)

        run_tool = paybond_runtime_tool_call_adapter(
            guard,
            operation=lambda call: call["name"],
            requested_spend_cents=lambda call: int(call["requested_spend_cents"]),
            execute=lambda call: book_hotel(
                str(call["city"]),
                int(call["nightly_budget_cents"]),
            ),
        )

        reservation = await run_tool(
            {
                "name": "travel.book_hotel",
                "requested_spend_cents": 18_700,
                "city": "Lisbon",
                "nightly_budget_cents": 18_700,
            }
        )

        submitted = await paybond.intents.submit_evidence(
            UUID(str(intent_id)),
            {"reservation": reservation},
            payee_did=os.environ["APP_PAYEE_DID"],
            payee_signing_seed=seed32_from_hex("APP_PAYEE_SEED_HEX"),
            recognition_proof=required_json_env("APP_EVIDENCE_RECOGNITION_PROOF_JSON"),
            idempotency_key=f"evidence:{intent_id}",
        )

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


asyncio.run(main())

Where agent SDKs fit

The direct adapter call above uses the same objects that a real agent runtime uses:

  • paybond.spend_guard(...) carries (harbor, intent_id, capability_token).
  • paybond_runtime_tool_call_adapter(...) performs Harbor POST /verify.
  • The adapter's operation resolver becomes the Paybond operation string, so travel.book_hotel must appear in the intent allow-list exactly.

When you wire this into a live agent run, pass the runtime's tool-call object into the adapter and map denials with on_deny if the framework expects a structured tool error instead of an exception.

Stablecoin rail variant

Set APP_SETTLEMENT_RAIL="x402_usdc_base" to request the Base / USDC rail. The example keeps the commercial amount in USD (budget["currency"] == "usd", amount_cents, max_spend_usd) while Harbor handles x402 funding and settlement in USDC on Base.

If create does not return capability_token, the example calls paybond.intents.fund, prints the payment_required challenge, and retries after you provide APP_X402_PAYMENT_SIGNATURE.

Expected Harbor outcome

  • create may return capability_token immediately, or the example may obtain it through paybond.intents.fund on x402_usdc_base.
  • 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.
  • Missing APP_X402_PAYMENT_SIGNATURE: Harbor returned an x402 challenge and is waiting for a signed retry.
  • Guardrail rejection: allowed_tools did not match the tool name, spend exceeded the capability, or the token is invalid.
  • Tenant mismatch: treat as a critical tenant-isolation error; 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