paybondpaybond
Sign in

Python SDK reference

Public Paybond Kit Python API surface, including async sessions, intent helpers, Signal and fraud reads, protocol and A2A helpers, MCP, and typed errors.

This page documents the public Python SDK. Use it when you need the exact coroutine signatures, result types, and error classes exposed by paybond-kit.

For a first paid-tool guardrail integration, start with One-command guardrails. For a guided production integration, use the Python quickstart. For the underlying HTTP contract, use the Harbor API reference and API documentation.

This SDK currently covers the Harbor application lifecycle, tenant-scoped ledger reads, protocol helpers, A2A discovery, read-only tenant-bound Signal reads, fraud review surfaces, and the packaged MCP server.

All I/O is async. The Kit does not expose a sync façade today.

Package and runtime

paybond-kit supports Python 3.11+ and uses httpx.AsyncClient for network I/O.

pip install paybond-kit

Optional extras:

  • paybond-kit[mcp] — installs the stdio MCP server dependency.
  • paybond-kit[langgraph] — installs LangGraph hook dependencies.

The package also exposes:

  • paybond_kit.mcp_server — programmatic MCP server settings and builder helpers.
  • paybond-kit-login — sandbox device approval CLI that writes PAYBOND_API_KEY to .env.local.
  • paybond-mcp-server — the packaged stdio MCP server CLI.
  • paybond-kit-init — a scaffold command for Paybond guardrail integration helpers.

Module entry point

from paybond_kit import (
    Paybond,
    PaybondIntents,
    PaybondCapabilityBinding,
    PaybondSpendGuard,
    PaybondSpendDeniedError,
    authorize_spend,
    guard_tool,
    paybond_agent_tool_spend_guard,
    paybond_runtime_neutral_tool_spend_guard,
    paybond_runtime_tool_call_adapter,
    paybond_langgraph_tool_spend_guard,
    paybond_mcp_tool_spend_guard,
    ServiceAccountSignalSession,
    ServiceAccountFraudSession,
    GatewaySandboxGuardrailsClient,
    SandboxGuardrailBootstrapResult,
    SandboxGuardrailEvidenceResult,
    GatewayHarborClient,
    HarborClient,
    GatewaySignalClient,
    GatewayFraudClient,
    GatewayA2AClient,
    GatewayProtocolClient,
    FundIntentResult,
    IntentFundingResult,
    SettlementRail,
    VerifyCapabilityResult,
    HarborHttpError,
    GatewayAuthError,
    A2AHttpError,
    FraudHttpError,
    ProtocolHttpError,
    SignalHttpError,
    TenantBindingError,
    AgentMandateV1,
    SignedAgentMandateV1,
    AgentRecognitionProofV1,
    ProtocolTransportBindingV1,
    ProtocolAuthorizationReceiptV1,
    ProtocolSettlementReceiptV1,
    ImportAgentMandateV1Result,
    VerifyProtocolReceiptV1Result,
    sign_payee_evidence_binding,
)

MCP is exposed from a submodule:

from paybond_kit.mcp_server import (
    PaybondMCPSettings,
    build_mcp_server,
    main as run_paybond_mcp_server,
)

High-level client

Paybond.open

@classmethod
async def Paybond.open(
    cls,
    *,
    api_key: str,                      # "paybond_sk_live_…" or "paybond_sk_sandbox_…" service-account key
    expected_environment: Literal["live", "sandbox"] | None = None,
    max_retries: int = 3,
) -> Paybond: ...

Resolves the service-account principal at the hosted Gateway, caches the echoed tenant_id, and returns a Paybond instance bound to that tenant realm. Hosted integrations only need api_key.

Set expected_environment to fail fast when a deployment is accidentally pointed at a live key from sandbox, or a sandbox key from live.

Raises GatewayAuthError on any tenant-principal lookup or credential failure.

Tenant isolation: the returned session is bound to the Gateway-derived tenant_id. Do not reuse the instance across tenants — build one per (tenant, service account).

Paybond instance

@dataclass
class Paybond:
    harbor: GatewayHarborClient    # tenant-bound Gateway Harbor surface
    signal: GatewaySignalClient    # tenant-bound Signal reads
    fraud: GatewayFraudClient      # tenant-bound fraud reads and review events
    a2a: GatewayA2AClient          # A2A discovery reads
    protocol: GatewayProtocolClient # protocol-v2 and recognition-gated helpers
    guardrails: GatewaySandboxGuardrailsClient # sandbox-only first guardrail helpers
    intents: PaybondIntents          # ergonomic intent helpers

    async def authorize_spend(
        self,
        *,
        intent_id: UUID,
        token: str,
        operation: str,
        requested_spend_cents: int = 0,
    ) -> VerifyCapabilityResult: ...
    def spend_guard(self, intent_id: UUID, capability_token: str) -> PaybondSpendGuard: ...
    async def aclose(self) -> None: ...
  • paybond.harbor — the tenant-bound Gateway-backed Harbor surface. Use it for capability verification and recognition-gated Harbor proxy calls.
  • paybond.signal — the tenant-bound GatewaySignalClient.
  • paybond.fraud — the tenant-bound GatewayFraudClient.
  • paybond.a2a — the GatewayA2AClient for A2A discovery.
  • paybond.protocol — the tenant-bound GatewayProtocolClient for protocol-v2 receipts, mandate import, and recognition-gated gateway mutations.
  • paybond.guardrails — the sandbox-only GatewaySandboxGuardrailsClient for first paid-tool guardrail integration.
  • paybond.intents — see PaybondIntents.
  • paybond.aclose() — release HTTP resources for Harbor, Signal, fraud, A2A, and protocol clients. Always await in a finally block or async with contextlib.aclosing(...).

GatewaySandboxGuardrailsClient

paybond.guardrails wraps the Free Developer sandbox guardrail routes. These routes derive tenant scope from the service-account API key, reject caller-supplied tenant IDs, and use simulator settlement only.

async def bootstrap_sandbox(
    *,
    operation: str,
    requested_spend_cents: int,
    currency: str | None = None,
    evidence_schema: Mapping[str, Any] | None = None,
    metadata: Mapping[str, Any] | None = None,
    idempotency_key: str | None = None,
) -> SandboxGuardrailBootstrapResult: ...

async def submit_sandbox_evidence(
    intent_id: UUID,
    payload: Mapping[str, Any] | None = None,
    *,
    artifacts: list[str] | None = None,
    operation: str | None = None,
    requested_spend_cents: int | None = None,
    metadata: Mapping[str, Any] | None = None,
    idempotency_key: str | None = None,
) -> SandboxGuardrailEvidenceResult: ...

Both result dataclasses include tenant_id, intent_id, operation, requested_spend_cents, and sandbox_lifecycle_status; bootstrap also returns the capability_token used with paybond.spend_guard(...).

PaybondIntents

paybond.intents wraps the principal/payee signing flows plus x402 funding. All methods derive tenant scope from the bound Harbor client; callers never pass a tenant id.

paybond.intents.create

async def create(
    self,
    *,
    principal_did: str,
    principal_signing_seed: bytes,      # exactly 32 bytes
    recognition_proof: Mapping[str, Any],
    payee_did: str,
    budget: Mapping[str, Any],          # canonical JSON (e.g. {"currency": ..., "max_spend_usd": ...})
    predicate: Mapping[str, Any],       # predicate_dsl (version 1 and root node)
    currency: str,
    amount_cents: int,
    evidence_schema: Mapping[str, Any], # JSON schema for evidence payloads
    deadline_rfc3339: str,
    allowed_tools: list[str],
    settlement_rail: SettlementRail,       # principal-signed rail request; destination stays server-owned
    intent_id: UUID | None = None,      # defaults to uuid4()
    predicate_ref: str = "",            # managed-policy digest when set
    idempotency_key: str | None = None,
) -> dict[str, Any]: ...

Builds a principal-signed intent body via the bundled _native.build_signed_create_intent_json and POSTs it through Gateway POST /harbor/intents.

paybond.intents.create_spend_intent(**kwargs) is a spend-oriented alias for the same method. Use it in agent-facing code when the user asked for agent spend controls, delegated spend guardrails, or tool-call budget checks.

Requires the bundled native signing extension. Published wheels include it. Without it, an ImportError is raised on the first call.

Retries honor idempotency_key on transient 5xx/429. Reusing a key with a different body returns HTTP 409 from Harbor (see docs/api/harbor-idempotency-openapi.yaml).

allowed_tools is a caller-defined allow-list of delegated operation names such as travel.planner.plan or crm.contact.enrich. Paybond does not define these strings; Harbor only checks that later verify calls use a matching value.

settlement_rail is required and included in the principal signature. It requests one allowed rail for the new intent. Harbor validates it against the tenant's canonical settlement config, resolves the destination server-side, and snapshots the effective routing onto the intent.

paybond.intents.fund

async def fund(
    self,
    intent_id: UUID,
    *,
    recognition_proof: Mapping[str, Any],
    payment_signature: str | None = None,
    idempotency_key: str | None = None,
) -> FundIntentResult: ...

Advances Gateway POST /harbor/intents/{intent_id}/fund for x402_usdc_base intents. Returns structured 402 challenges (payment_required), 202 pending states, and 200 funded results without forcing callers to catch those statuses as exceptions.

paybond.intents.submit_evidence

async def submit_evidence(
    self,
    intent_id: UUID,
    payload: Mapping[str, Any],         # evidence JSON matching the intent's schema
    *,
    payee_did: str,
    payee_signing_seed: bytes,          # exactly 32 bytes
    recognition_proof: Mapping[str, Any],
    artifacts_blake3_hex: list[str] | None = None,
    submitted_at_rfc3339: str | None = None,   # defaults to now (UTC)
    idempotency_key: str | None = None,
) -> dict[str, Any]: ...

Signs the payee binding via sign_payee_evidence_binding and POSTs it through Gateway POST /harbor/intents/{intent_id}/evidence.

Gateway-backed Harbor mutations require a replay-safe recognition proof bound to the exact method, path, purpose, tenant, and request body.

The high-level helper defaults artifacts_blake3_hex to [] and submitted_at_rfc3339 to the current UTC second. The low-level sign_payee_evidence_binding helper still requires concrete values because they are part of the signed bytes.

The response includes state, tenant, and an optional predicate_passed; a False here is not an error — the predicate evaluation failed and the intent moved to evidence_submitted with the recorded failure.

Verify (Gateway-enriched)

The Kit exposes capability verification on the underlying Harbor client rather than on paybond.intents. On hosted Gateway sessions, POST /verify runs Harbor capability validation first, then applies tenant spend policy before returning the final allow/deny outcome.

await paybond.harbor.verify_capability(
    intent_id=intent_id,
    token=capability_token,
    operation="travel.book_hotel",
    requested_spend_cents=18_700,
    vendor_id="vendor_acme",
    task_id="task_42",
    workflow_id="wf_travel",
    tool_call_id="call_1",
    tool_name="travel.book_hotel",
    currency="usd",
    agent_subject="agent:ops-bot",
    approval_token=approval_token,  # after operator approval
    idempotency_key="verify-key-1",
)

Pass vendor, task, workflow, tool, and agent metadata when tenant spend policy evaluates rolling caps, anomaly rules, or approval thresholds against those scopes. After an operator approves a held request in the console, retry with the same operation, amount, and metadata plus approval_token.

Returns a VerifyCapabilityResult with allow=False when Harbor or Gateway denies — that is not raised. When Gateway policy requires human approval first, inspect result.approval_required and usually decision_id / approval_request_id; handle that separately from a hard policy denial (see PaybondSpendGuard).

Confirm settlement

Settlement confirmation is an operator-tier action and is not wrapped by paybond.intents. For protocol-driven flows, use the recognition-gated paybond.protocol.confirm_harbor_settlement helper:

await paybond.protocol.confirm_harbor_settlement(
    str(intent_id),
    body={},   # Harbor derives release/refund from the recorded predicate result.
    recognition_proof=recognition_proof,
    idempotency_key=settlement_idempotency_key,
)

The helper calls the Gateway /harbor/intents/{intent_id}/settlement/confirm route with x-tenant-id and x-paybond-agent-recognition-proof. Gateway still enforces the protocol recognition proof, tenant scope, and operator/administrative policy. Harbor does not let this route choose release or refund; it confirms the settlement action implied by the evidence evaluation already stored on the intent. Prefer the Operator Console for human-reviewed settlement.

Gateway Harbor client

class GatewayHarborClient:
    def __init__(
        self,
        gateway_base_url: str,
        tenant_id: str,
        *,
        static_gateway_bearer_token: str,
        max_retries: int = 3,
    ) -> None: ...

    @property
    def tenant_id(self) -> str: ...

    async def verify_capability(...) -> VerifyCapabilityResult: ...

    async def complete_spend_decision(
        self,
        *,
        decision_id: str,
        outcome: Literal["consumed", "released"],
    ) -> None: ...

    async def create_intent(..., recognition_proof: Mapping[str, Any], ...) -> dict[str, Any]: ...
    async def aclose(self) -> None: ...

Paybond.open(...) constructs this for hosted integrations. Build it directly only when you already have a trusted tenant id from Gateway principal introspection.

HarborClient

Tenant-bound async HTTP client. Sends x-tenant-id on every request; verifies Harbor echoes the same tenant and the requested intent_id on POST /verify (confused-deputy hardening — raises TenantBindingError on mismatch).

class HarborClient:
    def __init__(
        self,
        harbor_base: str,
        tenant_id: str,
        *,
        harbor_bearer_supplier: Callable[[], Awaitable[str | None]] | None = None,
        static_harbor_bearer_token: str | None = None,
        max_retries: int = 3,
        request_timeout_sec: float = 30.0,
    ) -> None: ...

    @property
    def tenant_id(self) -> str: ...

    async def verify_capability(
        self,
        *,
        intent_id: UUID,
        token: str,
        operation: str,
        requested_spend_cents: int = 0,
    ) -> VerifyCapabilityResult: ...           # harborVerifyCapability

    async def create_intent(
        self,
        body: dict[str, Any],
        *,
        idempotency_key: str | None = None,
    ) -> dict[str, Any]: ...                    # harborCreateIntent

    async def fund_intent(
        self,
        intent_id: UUID,
        *,
        payment_signature: str | None = None,
        idempotency_key: str | None = None,
    ) -> FundIntentResult: ...                  # harborFundIntent

    async def submit_evidence(
        self,
        intent_id: UUID,
        evidence_body: dict[str, Any],
        *,
        idempotency_key: str | None = None,
    ) -> dict[str, Any]: ...                    # harborSubmitEvidence

    async def get_ledger_tip(self) -> dict[str, Any]: ...
    async def get_ledger_authority(self) -> dict[str, Any]: ...
    async def get_ledger_events(
        self,
        *,
        after_seq: int = 0,
        limit: int = 64,
    ) -> dict[str, Any]: ...
    async def get_ledger_merkle_latest(self) -> dict[str, Any]: ...

    async def aclose(self) -> None: ...

Retries 429 / 500 / 502 / 503 / 504 with exponential backoff and jitter, honoring a bounded Retry-After. All other 4xx/5xx raise HarborHttpError.

FundIntentResult

Structured return type for paybond.intents.fund(...) and harbor.fund_intent(...).

  • status_code: 402, 202, or 200
  • payment_required: x402 challenge header when status_code == 402
  • payment_response: optional Coinbase receipt header on 202 or 200
  • intent_id, tenant, state
  • settlement_rail, currency, amount_cents
  • funded: whether Harbor has transitioned the intent into the funded state
  • funding: optional IntentFundingResult
  • capability_token: present when Harbor already minted the funded capability

IntentFundingResult

Normalized Harbor funding payload fields:

  • settlement_rail
  • harbor_fund_endpoint
  • status
  • payment_session_id
  • payment_url
  • stripe_payment_intent_id, client_secret, stripe_connect_destination
  • stripe_customer_id, latest_charge_id, payment_method_id, mandate_id
  • financial_connections_account_id, bank_last4, bank_fingerprint, bank_name
  • asset, network
  • authorization_id, capture_id, void_id, transfer_id, refund_id
  • expected_debit_date, payment_reference, refund_reference, refund_reference_status
  • source_address, target_address
  • authorization_expires_at, capture_expires_at, refund_expires_at
  • onchain_transaction_hashes

SettlementRail

SettlementRail = Literal["stripe_connect", "stripe_ach_debit", "x402_usdc_base"]

Public type alias for the only supported client-requestable settlement rails. The SDK does not expose any client-side destination fields; Harbor resolves destinations from tenant-owned settlement config.

Ledger reads

The SDK includes protected tenant-scoped provenance reads on the low-level direct HarborClient. Hosted Paybond.open(...) integrations should prefer Gateway's public compliance surface, verifier packs, verification results, and audit exports instead of a raw ledger event stream.

  • get_ledger_tip()GET /ledger/v1/tip
  • get_ledger_authority()GET /ledger/v1/authority
  • get_ledger_events(after_seq=..., limit=...)GET /ledger/v1/events
  • get_ledger_merkle_latest()GET /ledger/v1/merkle/latest

Each method validates the echoed tenant_id against the bound client tenant and fails closed on mismatch.

Signal reads

The Python SDK ships a tenant-bound gateway Signal client for the supported read-only V1.0.1 surfaces.

GatewaySignalClient

class GatewaySignalClient:
    def __init__(
        self,
        gateway_base_url: str,
        tenant_id: str,
        *,
        static_gateway_bearer_token: str,
        max_retries: int = 3,
        request_timeout_sec: float = 30.0,
    ) -> None: ...

    @property
    def tenant_id(self) -> str: ...

    async def get_reputation_receipt(
        self,
        operator_did: str,
        *,
        score_version: str | None = None,
    ) -> SignalReceiptEnvelope | None: ...

    async def get_portfolio_summary(
        self,
        *,
        score_version: str | None = None,
    ) -> SignalPortfolioSummary: ...

    async def get_signed_portfolio_artifact(
        self,
        *,
        score_version: str | None = None,
    ) -> SignalSignedPortfolioArtifact: ...

    async def get_operator_explanation(
        self,
        operator_did: str,
        *,
        score_version: str | None = None,
    ) -> dict[str, Any] | None: ...

    async def get_operator_review_status(
        self,
        operator_did: str,
        *,
        score_version: str | None = None,
    ) -> dict[str, Any] | None: ...

    async def aclose(self) -> None: ...

This client validates the echoed tenant_id and operator_did on every response. 404 remains a None result for receipt, explanation, and review-status lookups; other non-success statuses raise SignalHttpError. get_signed_portfolio_artifact() returns the tenant-scoped signed portfolio export that partners and verifiers can carry across systems without turning Signal into a public leaderboard surface.

ServiceAccountSignalSession

@dataclass
class ServiceAccountSignalSession:
    signal: GatewaySignalClient

    @classmethod
    async def open(
        cls,
        *,
        gateway_base_url: str,
        api_key: str,
        principal_path: str = "/v1/auth/principal",
        expected_environment: Literal["live", "sandbox"] | None = None,
        max_retries: int = 3,
    ) -> ServiceAccountSignalSession: ...

    async def aclose(self) -> None: ...

This is the easiest tenant-safe path for partner developers who only need Signal reads. The session resolves the tenant from GET /v1/auth/principal using the same service-account credential instead of relying on ad hoc tenant headers.

Fraud review and metrics

GatewayFraudClient

class GatewayFraudClient:
    def __init__(
        self,
        gateway_base_url: str,
        tenant_id: str,
        *,
        static_gateway_bearer_token: str,
        max_retries: int = 3,
        request_timeout_sec: float = 30.0,
    ) -> None: ...

    @property
    def tenant_id(self) -> str: ...

    async def get_fraud_assessment(
        self,
        operator_did: str,
        *,
        score_version: str | None = None,
    ) -> SignalFraudAssessmentResponse | None: ...

    async def list_fraud_review_queue(
        self,
        *,
        state: str | None = None,
        severity: str | None = None,
        limit: int | None = None,
        score_version: str | None = None,
    ) -> SignalFraudReviewQueueResponse: ...

    async def get_fraud_metrics(
        self,
        *,
        window: str | None = None,
        score_version: str | None = None,
    ) -> SignalFraudMetricsResponse: ...

    async def get_fraud_release_gate_config(
        self,
        *,
        score_version: str | None = None,
    ) -> SignalFraudReleaseGateConfigResponse: ...

    async def set_fraud_release_gate_mode(
        self,
        mode: str,
    ) -> SignalFraudReleaseGateConfigResponse: ...

    async def record_fraud_review_event(
        self,
        operator_did: str,
        event: SignalFraudReviewEventInput,
        *,
        score_version: str | None = None,
    ) -> SignalFraudReviewEventResponse: ...

    async def aclose(self) -> None: ...

The fraud client validates echoed tenant_id and, for operator-scoped methods, operator_did. SignalFraudMetricsResponse includes rolling-window backtest and monitoring fields such as false_positive_rate_bps, labeled_coverage_bps, critical_signal_hold_candidate_count, stale_label_gap_seconds, stale_signal_family_label_gap_count, and backtest_summary. SignalFraudSignal preserves the fraud_signal_version=1.0.7 additive metadata fields signal_source, first_seen_at, last_seen_at, evidence_binding_strength, provider_event_refs, and intent_refs when Gateway returns them. set_fraud_release_gate_mode() accepts only review_only or critical_hold; Gateway still enforces tenant-admin access and keeps review_only as the default. record_fraud_review_event() only accepts review workflow/outcome event types, does not expose settlement or money-movement events, and can pass optional signal-level feedback context through signalCode/signal_code, intentId/intent_id, and providerEventId/provider_event_id.

ServiceAccountFraudSession

@dataclass
class ServiceAccountFraudSession:
    fraud: GatewayFraudClient

    @classmethod
    async def open(
        cls,
        *,
        gateway_base_url: str,
        api_key: str,
        principal_path: str = "/v1/auth/principal",
        expected_environment: Literal["live", "sandbox"] | None = None,
        max_retries: int = 3,
    ) -> ServiceAccountFraudSession: ...

    async def aclose(self) -> None: ...

Paybond.open(...) also exposes gateway clients as paybond.signal, paybond.fraud, paybond.a2a, and paybond.protocol.

A2A discovery

GatewayA2AClient

class GatewayA2AClient:
    def __init__(
        self,
        gateway_base_url: str,
        *,
        static_gateway_bearer_token: str | None = None,
        max_retries: int = 3,
        request_timeout_sec: float = 30.0,
    ) -> None: ...

    async def get_agent_card(self) -> dict[str, Any]: ...
    async def get_task_contracts(self) -> dict[str, Any]: ...
    async def get_task_contract(self, contract_id: str) -> dict[str, Any]: ...
    async def aclose(self) -> None: ...

paybond.a2a is created automatically by Paybond.open and carries the same service-account bearer token when available. Use it for:

  • GET /.well-known/agent-card.json
  • GET /protocol/v2/a2a/task-contracts
  • GET /protocol/v2/a2a/task-contracts/{contract_id}

Non-success statuses raise A2AHttpError. See Agent integrations and V2 protocol trust.

Protocol v2 and recognition-gated gateway mutations

GatewayProtocolClient

class GatewayProtocolClient:
    def __init__(
        self,
        gateway_base_url: str,
        tenant_id: str,
        *,
        static_gateway_bearer_token: str | None = None,
        max_retries: int = 3,
        request_timeout_sec: float = 30.0,
    ) -> None: ...

    @property
    def tenant_id(self) -> str: ...

    async def import_agent_mandate_v1(
        self,
        *,
        signed_mandate: SignedAgentMandateV1,
        intent_id: str,
        recognition_proof: AgentRecognitionProofV1 | dict[str, Any],
        transport_binding: ProtocolTransportBindingV1 | None = None,
    ) -> ImportAgentMandateV1Result: ...

    async def get_settlement_receipt_v1(
        self,
        receipt_id: str,
    ) -> ProtocolSettlementReceiptV1: ...

    async def verify_protocol_receipt_v1(
        self,
        receipt: ProtocolAuthorizationReceiptV1 | ProtocolSettlementReceiptV1 | dict[str, Any],
    ) -> VerifyProtocolReceiptV1Result: ...

    async def create_harbor_intent(
        self,
        *,
        body: dict[str, Any],
        recognition_proof: AgentRecognitionProofV1 | dict[str, Any],
        idempotency_key: str | None = None,
    ) -> dict[str, Any]: ...

    async def fund_harbor_intent(
        self,
        intent_id: str,
        *,
        recognition_proof: AgentRecognitionProofV1 | dict[str, Any],
        payment_signature: str | None = None,
        idempotency_key: str | None = None,
    ) -> dict[str, Any]: ...

    async def submit_harbor_evidence(
        self,
        intent_id: str,
        *,
        body: dict[str, Any],
        recognition_proof: AgentRecognitionProofV1 | dict[str, Any],
        idempotency_key: str | None = None,
    ) -> dict[str, Any]: ...

    async def confirm_harbor_settlement(
        self,
        intent_id: str,
        *,
        body: dict[str, Any],  # usually {}; included in the recognition envelope
        recognition_proof: AgentRecognitionProofV1 | dict[str, Any],
        idempotency_key: str | None = None,
    ) -> dict[str, Any]: ...

    async def aclose(self) -> None: ...

paybond.protocol is created automatically by Paybond.open. It sends x-tenant-id, adds Authorization when a gateway bearer token is configured, and base64url-encodes the recognition proof into x-paybond-agent-recognition-proof for recognition-gated Harbor mutation helpers.

Paybond is AP2-aligned at the protocol boundary. On mandate import, transport_binding["source_protocol"] is optional and Gateway defaults it to ap2 for imported external mandates and exported protocol receipts.

Gateway HTTP failures raise ProtocolHttpError with parsed error_code / error_message when Gateway returns a JSON error envelope. Tenant, intent, and receipt echo mismatches raise RuntimeError and should be treated as critical tenant-isolation failures.

Core protocol types

The SDK exports the protocol shapes used by GatewayProtocolClient:

  • AgentMandateV1 / SignedAgentMandateV1
  • AgentRecognitionProofV1
  • ProtocolTransportBindingV1
  • ProtocolAuthorizationReceiptV1
  • ProtocolSettlementReceiptV1
  • ImportAgentMandateV1Result
  • VerifyProtocolReceiptV1Result

MCP server submodule

@dataclass(frozen=True)
class PaybondMCPSettings:
    api_key: str
    gateway_base_url: str = "https://api.paybond.ai"
    principal_path: str = "/v1/auth/principal"
    max_retries: int = 3

    @classmethod
    def from_env(cls, env: Mapping[str, str] | None = None) -> PaybondMCPSettings: ...

def build_mcp_server(settings: PaybondMCPSettings | None = None) -> Any: ...
def run_paybond_mcp_server(argv: list[str] | None = None) -> int: ...

The packaged CLI is usually simpler:

paybond-mcp-server

Required environment is PAYBOND_API_KEY. See the MCP server guide for the supported tool list and host configuration.

PaybondCapabilityBinding

Most application code should use paybond.spend_guard(intent_id, capability_token) instead. This binding remains available for framework adapters that require a run-context object, such as the LangGraph hook.

@dataclass
class PaybondCapabilityBinding:
    harbor: HarborClient
    intent_id: UUID
    capability_token: str

    async def verify_spend_capability(
        self,
        *,
        operation: str,
        requested_spend_cents: int = 0,
    ) -> VerifyCapabilityResult: ...
    async def authorize_spend(
        self,
        *,
        operation: str,
        requested_spend_cents: int = 0,
    ) -> VerifyCapabilityResult: ...

Binds (harbor, intent_id, capability_token) so agent framework hooks have a single tenant-safe handle to verify against. Use paybond.spend_guard(...) for the runtime-neutral adapter; use this binding when a framework hook expects a context object. See Agent integrations.

PaybondSpendGuard

guard = paybond.spend_guard(intent_id, capability_token)

guarded_tool = guard.guard_tool(
    operation="travel.book_hotel",
    requested_spend_cents=20_000,
    vendor_id="vendor_acme",
    tool_name="travel.book_hotel",
    agent_subject="agent:ops-bot",
    handler=book_hotel,
)

PaybondSpendGuard is a discoverable wrapper around Gateway-enriched capability verification. It calls verify_capability before the handler runs:

  • raises PaybondSpendDeniedError on hard denials (allow=False without approval required)
  • raises PaybondSpendApprovalRequiredError when result.approval_required is true
  • after a successful authorization, guard_tool runs the handler and then calls complete_spend_decision with outcome consumed on success or released when the handler raises (best-effort release)

If you call authorize_spend or assert_spend_authorized directly instead of guard_tool, call complete_spend_authorization yourself when the guarded work finishes or aborts.

Standalone helpers are also exported. They accept a source with harbor, intent_id, and capability_token attributes, including PaybondCapabilityBinding:

  • authorize_spend(source, operation=..., requested_spend_cents=..., vendor_id=..., approval_token=..., ...)
  • guard_tool(source, operation=..., requested_spend_cents=..., handler=...)
  • paybond_agent_tool_spend_guard(source, operation=..., handler=...)
  • paybond_runtime_neutral_tool_spend_guard(source, operation=..., handler=...)
  • paybond_langgraph_tool_spend_guard(source, operation=..., handler=...)
  • paybond_mcp_tool_spend_guard(source, operation=..., handler=...)

Optional verify metadata on authorize_spend, verify_spend_capability, and guard_tool: vendor_id, task_id, workflow_id, tool_call_id, tool_name, currency, agent_subject, approval_token, idempotency_key.

For agent SDKs or custom runtimes that hand your application a tool-call object and an executor, use paybond_runtime_tool_call_adapter(...):

run_tool = paybond_runtime_tool_call_adapter(
    guard,
    operation=lambda call: call["name"],
    requested_spend_cents=lambda call: int(call.get("spend_cents", 0)),
    execute=execute_tool_call,
    on_deny=lambda result, _: {
        "status": "blocked",
        "reason": result.message or result.code,
    },
)

Scaffolding

Sandbox credential setup:

paybond-kit-login

The login command supports --env-file, --gateway, --no-open, and --force. It writes .env.local with mode 0600, adds the default .env.local target to .gitignore when needed, refuses custom unignored env files inside git repos, and prints only masked key identity plus the target sandbox tenant.

paybond-kit-init \
  --preset paid-tool-guard \
  --framework provider-agnostic \
  --out paybond_paid_tool_guard.py

Supported framework labels are generic, provider-agnostic, openai, claude, anthropic, gemini, google-ai, langgraph, and mcp. The scaffold writes reusable guardrail helpers and expects your application to provide the paid-tool handler; it does not manage signing keys or payment wallets.

Result types

VerifyCapabilityResult

Gateway-enriched verify outcome. Harbor-only deployments populate the core fields; hosted Gateway sessions may also return spend-policy metadata.

@dataclass(frozen=True)
class VerifyCapabilityResult:
    allow: bool
    audit_id: UUID
    tenant: str
    intent_id: UUID
    code: str | None
    message: str | None
    decision_id: UUID | None = None
    approval_request_id: UUID | None = None
    policy_version: int | None = None
    reason_codes: tuple[str, ...] = ()
    spend_scope: dict[str, str] | None = None
    remaining_cents: int | None = None
    retry_after: int | None = None

    @property
    def approval_required(self) -> bool: ...
FieldMeaning
allowFinal authorization decision after Harbor capability checks and Gateway spend policy.
approval_requiredPolicy hold — not a hard deny. Surface to operators and retry with approval_token after approval.
decision_idImmutable authorization decision id; used to finalize scope reservations after tool execution.
approval_request_idPending approval queue item when quorum is incomplete.
reason_codesMachine-readable policy outcomes (for example approval_threshold_exceeded, daily_tenant_cap_exceeded).
spend_scope / remaining_centsScope that governed the check and optional remaining budget headroom.

PaybondSpendDeniedError / PaybondSpendApprovalRequiredError

PaybondSpendGuard.assert_spend_authorized and guard_tool raise PaybondSpendDeniedError when allow is false and approval is not required. They raise PaybondSpendApprovalRequiredError when approval_required is true. Both exceptions expose a result: VerifyCapabilityResult attribute for logging and operator UX.

Evidence response

paybond.intents.submit_evidence returns a plain dict[str, Any] with at least the following keys:

KeyTypeMeaning
intent_idstr (UUID)Harbor-echoed intent id; compared against the request.
tenantstrHarbor-echoed tenant realm.
statestrNew intent state (for example "evidence_submitted").
predicate_passedbool (optional)True if the predicate VM accepted the evidence.

ImportAgentMandateV1Result

class ImportAgentMandateV1Result(TypedDict):
    valid: bool
    intent_id: str
    mandate_digest_sha256_hex: str
    mandate: AgentMandateV1
    authorization_receipt: ProtocolAuthorizationReceiptV1

Returned by GatewayProtocolClient.import_agent_mandate_v1. The client verifies the echoed intent_id and mandate tenant before returning.

VerifyProtocolReceiptV1Result

class VerifyProtocolReceiptV1Result(TypedDict):
    valid: bool
    kind: str
    receipt_id: str
    tenant_id: str
    receipt: dict[str, Any]

Returned by GatewayProtocolClient.verify_protocol_receipt_v1 for authorization and settlement receipt verification.

Canonical signing helpers

sign_payee_evidence_binding

def sign_payee_evidence_binding(
    *,
    tenant_id: str,
    intent_id: UUID,
    payee_did: str,
    payload: dict[str, Any],
    artifacts_blake3_hex: list[str],
    submitted_at_rfc3339: str,
    payee_signing_seed: bytes,
) -> dict[str, Any]: ...

Builds the signed evidence wire body used by operation harborSubmitEvidence. Exported for advanced callers that pre-build evidence bodies outside of paybond.intents.submit_evidence.

Principal-intent signing is performed in the native extension via _native.build_signed_create_intent_json. Canonical JSON normalization, BLAKE3 digests, and Ed25519 signing match Paybond's canonical signing rules byte-for-byte.

Typed errors

All error classes extend RuntimeError and attach structured diagnostics. Never log raw tokens or API keys; the Kit performs no redaction itself.

HarborHttpError

class HarborHttpError(RuntimeError):
    status_code: int
    url: str
    body_text: str                # raw Harbor response body (may be JSON)

Raised by HarborClient when Harbor returns any non-success status after retries. Look for the machine-readable code field inside body_text — contracts are in the Error envelope reference.

A2AHttpError

class A2AHttpError(RuntimeError):
    status_code: int
    url: str
    body_text: str

Raised by GatewayA2AClient when A2A discovery routes return non-success statuses after retries.

ProtocolHttpError

class ProtocolHttpError(RuntimeError):
    status_code: int
    url: str
    body_text: str                 # raw gateway response body (may be JSON)
    error_code: str | None         # parsed gateway "error" field when present
    error_message: str | None      # parsed gateway "message" field when present

Raised by GatewayProtocolClient and paybond.protocol for gateway-backed protocol/v2/* calls plus recognition-gated gateway /harbor/* mutation helpers. The Kit parses JSON gateway error envelopes so callers can branch on explicit codes such as unregistered_key, tenant_mismatched_key, revoked_key, mandate_agent_key_mismatch, and protocol_binding_mismatch without reparsing body_text.

GatewayAuthError

class GatewayAuthError(RuntimeError):
    status_code: int | None
    body_text: str | None

Raised when Gateway rejects the API key, returns malformed JSON, or omits tenant_id.

FraudHttpError

class FraudHttpError(RuntimeError):
    status_code: int
    url: str
    body_text: str

Raised by GatewayFraudClient when gateway fraud routes return a non-success status after retries, excluding the 404 -> None read contract on fraud assessment lookups.

SignalHttpError

class SignalHttpError(RuntimeError):
    status_code: int
    url: str
    body_text: str

Raised by GatewaySignalClient when gateway Signal routes return a non-success status after retries, excluding the 404 -> None read contract on receipt, explanation, and review-status lookups.

TenantBindingError

class TenantBindingError(RuntimeError): ...

Raised when Harbor echoes a different tenant or intent than the Kit requested on protected Harbor calls such as POST /verify, POST /intents/{id}/fund, or ledger reads. Signal, fraud, and protocol clients raise RuntimeError for tenant, operator, mandate, or receipt echo mismatches. Treat all of these as critical tenant-isolation failures and do not retry.

Retries, idempotency, and logging

  • Harbor and protocol state-changing helpers accept idempotency_key. The same key with the same body is safe to retry on transient failures; the same key with a different body returns 409.
  • Log tenant_id, intent_id, status_code, url, correlation id (when present), and a redacted body_text. Never log the API key, Harbor JWT, or capability token.
  • Backoff is exponential with jitter, bounded at ~5s per attempt; Retry-After is honored up to 30s.