For a first paid-tool guardrail integration, start with the sandbox scaffold:
paybond-kit-init \
--preset paid-tool-guard \
--framework provider-agnostic \
--out paybond_paid_tool_guard.py
This Free Developer path is sandbox-only. It bootstraps a sandbox guardrail intent, wraps your paid-tool handler, submits simulator evidence, and does not generate a paid-tool implementation. See One-command guardrails for the focused walkthrough.
This quickstart shows the standard Paybond application flow in Python:
- Open an authenticated session against the Gateway with a service-account API key.
- Create a principal-signed intent.
- Read the funded intent's
capability_token, or fund an x402 intent through/fund. - Verify that capability before tool work.
- Submit signed evidence so Harbor can evaluate the predicate and transition the intent.
Every call below is tenant-scoped. The tenant comes from the Gateway exchange and is bound to the returned client, so you never pass a tenant ID by hand.
allowed_tools should contain your application's own operation names. Paybond does not define that catalog; Harbor simply checks that the later authorize_spend(operation=...) or verify_capability(operation=...) call matches one of the names attached to the intent.
For the full API surface, see the Python SDK reference.
Prerequisites
- Python 3.11+.
- A
paybond_sk_sandbox_…orpaybond_sk_live_…service-account API key issued for your tenant realm. For local sandbox work, runpaybond-kit-login; live production keys are created in Console by tenant admins and stored in your deployment secret manager. - For the principal-signed intent create flow: a 32-byte Ed25519 seed for the principal and a 32-byte seed for the payee bound to the intent.
Install
Install the published package:
pip install paybond-kit
Optional extras:
pip install "paybond-kit[langgraph]"
pip install "paybond-kit[mcp]"
pip install "paybond-kit[langgraph,mcp]"
Install only the extras your runtime needs. langgraph enables the LangGraph hook dependencies, and mcp enables the packaged stdio MCP server.
Published wheels bundle the native signing extension used by the high-level intent helpers. If you need a complete runnable application, use the examples linked from this section.
Sandbox login
paybond-kit-login
The login CLI starts a sandbox device approval flow, writes PAYBOND_API_KEY to .env.local with file mode 0600, adds the default .env.local target to .gitignore when needed, and refuses to overwrite an existing key unless --force is passed. Custom env-file paths inside a git repo must already be ignored. It prints the masked key identity and target sandbox tenant only; it never prints the raw key after writing.
Authenticated session
Paybond.open resolves the tenant realm from the hosted Gateway and returns tenant-bound Gateway, Harbor, Signal, fraud, A2A, and protocol clients.
The examples below assume a sandbox key; set expected_environment to "live" when using a paybond_sk_live_… key.
The paybond.harbor property is created by Paybond.open(...); use paybond.spend_guard(...) for normal tool guards and paybond.harbor directly only for lower-level Harbor calls.
import asyncio
import os
from paybond_kit import Paybond
async def main() -> None:
paybond = await Paybond.open(
api_key=os.environ["PAYBOND_API_KEY"],
expected_environment="sandbox",
)
try:
print("tenant realm:", paybond.harbor.tenant_id)
finally:
await paybond.aclose()
asyncio.run(main())
Key properties of the session:
- Tenant is derived, not supplied. The tenant realm is whatever the Gateway echoes. Do not read the tenant from request bodies or environment variables at call time.
- Gateway is the public surface. Harbor operations go through Gateway's tenant-scoped
/harborand/verifyroutes. - Sessions are single-tenant. Construct one
Paybondinstance per(tenant, service account). Never share across tenants or reuse across concurrent agent runs bound to different intents. - Release resources. Always await
paybond.aclose()so the underlyinghttpx.AsyncClientdrains.Paybond.openraisesGatewayAuthErroron any gateway or token failure.
Capability token source
Normal integrations do not supply capability tokens through environment variables. The token comes from the intent lifecycle:
paybond.intents.create(...)returnscapability_tokenwhen the selected rail funds the intent immediately.paybond.intents.fund(...)returnscapability_tokenafter anx402_usdc_basefunding challenge is satisfied.- If you do not pass
intent_idtocreate, read the generated id fromcreated["intent_id"].
Notes:
paybond.spend_guard(...).authorize_spend(...)and lower-levelverify_capabilitycalls go through GatewayPOST /verify. A200response withallow: Falseis a business denial, not an HTTP transport error.paybond.intents.submit_evidencesigns the payee evidence binding and posts it to GatewayPOST /harbor/intents/{id}/evidence. Reuse the sameidempotency_keywhen retrying the same submission.- The client validates tenant and intent echoes on protected reads and writes so mismatched responses fail closed.
- Keep
payee_signing_seedin a secret manager. The value must be exactly 32 bytes and must correspond to the payee key bound on the intent; any drift will be rejected server-side.
Recognition proofs
Every Gateway-backed Harbor mutation in these snippets takes a recognition_proof. Think of it as a short-lived signature that says: "this tenant-registered agent key is authorizing this exact Gateway request right now."
Paybond does not create or hand this proof to your app, and Kit does not generate it automatically. The proof is created by your trusted backend, KMS-backed signer, wallet service, or agent runner:
- A tenant admin registers the agent runtime's Ed25519 public key in Paybond's trusted agent key registry and assigns it a stable
key_id. - Your trusted component keeps the matching Ed25519 private key. Do not expose that key to a browser, model prompt, or untrusted tool.
- Immediately before a protected mutation, that component signs a fresh
AgentRecognitionProofV1for the exact request. - Kit base64url-encodes the finished proof object and sends it as
x-paybond-agent-recognition-proof. - Gateway verifies the signature against the registered public key, checks tenant/purpose/request binding, and rejects replayed nonces.
A typical app keeps this behind an internal helper or endpoint:
async def issue_agent_recognition_proof_v1(
*,
purpose: str,
method: str,
path: str,
body: dict[str, object],
) -> AgentRecognitionProofV1: ...
Generate a fresh proof after the request body is fixed:
| SDK call | Purpose | Request envelope |
|---|---|---|
paybond.intents.create(...) | harbor.intent.create | POST /harbor/intents, SHA-256 digest of the signed create body |
paybond.intents.fund(...) | harbor.intent.fund | POST /harbor/intents/{intent_id}/fund, SHA-256 digest of the empty object body |
paybond.intents.submit_evidence(...) | harbor.intent.evidence.submit | POST /harbor/intents/{intent_id}/evidence, SHA-256 digest of the signed evidence body |
Because the proof covers the body digest, the proof issuer must run where it can see or reproduce the final request body. If you prebuild Harbor bodies outside the high-level helpers, send them through the lower-level Gateway Harbor methods such as paybond.harbor.create_intent(...) and paybond.harbor.submit_evidence(...).
Each proof should include schema_version=1, kind="paybond.agent_recognition_proof_v1", the tenant-registered key_id, signature_algorithm="ed25519-sha256-json-v1", verifier_context.tenant_id=paybond.harbor.tenant_id, verifier_context.verifier_id="paybond-gateway", a request envelope, a unique nonce, a short issued_at / expires_at window of 10 minutes or less, and the Ed25519 digest/signature fields.
Only PAYBOND_API_KEY is Paybond-issued in these examples. The APP_* environment variables below are local sandbox/live quick-start placeholders for values owned by your application, such as agent DIDs, signing seeds, rail choices, wallet signatures, and proof JSON returned by your signer. They are not static production config. Production code should call your proof issuer for each create, fund, fund retry, and submit_evidence request.
Agent framework integrations
Paybond ships a runtime-neutral tool-call adapter for provider SDKs and custom orchestrators, plus a LangGraph hook for ToolNode deployments. The runtime-neutral adapter can use paybond.spend_guard(...); LangGraph uses a PaybondCapabilityBinding run-context object.
from paybond_kit import PaybondCapabilityBinding
from paybond_kit.agent_adapters import paybond_runtime_tool_call_adapter
from paybond_kit.langgraph_hooks import paybond_awrap_tool_call_capability
from langgraph.prebuilt import ToolNode
guard = paybond.spend_guard(intent_id, capability_token)
run_tool = paybond_runtime_tool_call_adapter(
guard,
operation=lambda call: call["name"],
execute=execute_tool_call,
)
binding = PaybondCapabilityBinding(
harbor=paybond.harbor,
intent_id=intent_id,
capability_token=capability_token,
)
tool_node = ToolNode(
tools=[], # your tools
awrap_tool_call=paybond_awrap_tool_call_capability(binding),
)
See Agent integrations for end-to-end examples and runtime-specific patterns.
Principal-signed intent create
paybond.intents.create builds a principal-signed intent body and submits it through Gateway POST /harbor/intents. intent_id can default to uuid4() when omitted, but this quickstart passes it explicitly so the recognition proof can bind to a stable request.
import asyncio
from datetime import datetime, timezone
import json
import os
from uuid import UUID, uuid4
from paybond_kit import Paybond
async def main() -> None:
principal_seed = bytes.fromhex(os.environ["APP_PRINCIPAL_SEED_HEX"]) # 32 bytes
payee_seed = bytes.fromhex(os.environ["APP_PAYEE_SEED_HEX"]) # 32 bytes
paybond = await Paybond.open(
api_key=os.environ["PAYBOND_API_KEY"],
expected_environment="sandbox",
)
try:
intent_id = uuid4()
create_recognition_proof = json.loads(os.environ["APP_INTENT_CREATE_RECOGNITION_PROOF_JSON"])
created = await paybond.intents.create(
principal_did="did:web:example.com#principal",
principal_signing_seed=principal_seed,
recognition_proof=create_recognition_proof,
payee_did="did:web:example.com#booker-agent",
budget={"currency": "usd", "max_spend_usd": 200},
predicate={"version": 1, "root": {"op": "true"}},
currency="usd",
amount_cents=20_000,
evidence_schema={"type": "object"},
deadline_rfc3339="2030-12-31T23:59:59Z",
allowed_tools=["travel.planner.plan", "travel.booker.purchase"],
settlement_rail="stripe_connect",
intent_id=intent_id,
idempotency_key=f"intent:{intent_id}",
)
created_intent_id = UUID(str(created["intent_id"]))
if created_intent_id != intent_id:
raise RuntimeError(f"intent mismatch: requested={intent_id} gateway={created_intent_id}")
capability_token = str(created.get("capability_token") or "")
if not capability_token:
raise RuntimeError("intent created without capability_token; fund the intent before verifying tools")
guard = paybond.spend_guard(intent_id, capability_token)
verified = await guard.authorize_spend(
operation="travel.booker.purchase",
requested_spend_cents=18_700,
)
if not verified.allow:
raise RuntimeError(
f"verify denied: {verified.code or 'deny'} {verified.message or ''}".strip()
)
# Only run the real action after Paybond authorizes the agent to do it.
submitted_at_rfc3339 = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
evidence_recognition_proof = json.loads(os.environ["APP_EVIDENCE_RECOGNITION_PROOF_JSON"])
await paybond.intents.submit_evidence(
intent_id,
{"flight_booked": True, "price": 187, "confirmation": "PNR-4GH271"},
payee_did="did:web:example.com#booker-agent",
payee_signing_seed=payee_seed,
recognition_proof=evidence_recognition_proof,
submitted_at_rfc3339=submitted_at_rfc3339,
idempotency_key=f"evidence:{intent_id}",
)
finally:
await paybond.aclose()
asyncio.run(main())
Choose intent_id, submitted_at_rfc3339, and idempotency keys before issuing request-bound recognition proofs so your signer can bind to the same request the SDK will send.
Managed-policy intent creation is not wrapped by this helper. If you need registry-bound signing, call paybond.harbor.create_intent with a pre-built body and use the Harbor policy registry.
allowed_tools is your namespace, not Paybond's. For example, if your runtime later verifies operation="travel.booker.purchase", that exact string must appear in the intent allow-list.
settlement_rail is required and principal-signed. It requests one allowed rail such as stripe_connect, stripe_ach_debit, or x402_usdc_base; you are not supplying a Stripe destination or x402 receive address. Harbor resolves the destination from the tenant's canonical settlement config and snapshots it onto the intent.
Intent pricing remains USD-denominated even on the stablecoin rail: keep budget["currency"] and currency as "usd" with cent-based amounts, while x402_usdc_base tells Harbor to settle that value in USDC on Base.
Stablecoin funding via /fund
For intents created on the x402_usdc_base rail, Harbor returns a funding object with payment-session metadata and the tenant-scoped harbor_fund_endpoint. The Kit wraps that flow with paybond.intents.fund.
The snippet below uses two app-owned helpers: issue_agent_recognition_proof_v1(...) creates a fresh request-bound proof, and x402_wallet.sign_payment(...) signs the x402 challenge returned by Harbor.
fund_recognition_proof = await issue_agent_recognition_proof_v1(
purpose="harbor.intent.fund",
method="POST",
path=f"/harbor/intents/{intent_id}/fund",
body={},
)
first = await paybond.intents.fund(intent_id, recognition_proof=fund_recognition_proof)
if first.status_code == 402:
if not first.payment_required:
raise RuntimeError("missing PAYMENT-REQUIRED challenge")
payment_signature = await x402_wallet.sign_payment(first.payment_required)
retry_recognition_proof = await issue_agent_recognition_proof_v1(
purpose="harbor.intent.fund",
method="POST",
path=f"/harbor/intents/{intent_id}/fund",
body={},
)
funded = await paybond.intents.fund(
intent_id,
recognition_proof=retry_recognition_proof,
payment_signature=payment_signature,
)
if not funded.capability_token:
raise RuntimeError("funding is pending; poll /fund with a fresh recognition proof before guarding tools")
print(funded.status_code, funded.funded, funded.capability_token)
elif first.capability_token:
print(first.status_code, first.funded, first.capability_token)
else:
raise RuntimeError("intent is not funded yet; poll /fund with a fresh recognition proof before guarding tools")
Notes:
issue_agent_recognition_proof_v1(...)is your trusted backend or agent-runner signer, not a Kit helper. Local sandbox/live quick-start scripts may loadAPP_FUND_RECOGNITION_PROOF_JSONandAPP_FUND_RETRY_RECOGNITION_PROOF_JSONfrom the environment, but production code should issue a freshAgentRecognitionProofV1for each/fundcall.- Generate the
AgentRecognitionProofV1withpurpose="harbor.intent.fund",verifier_context.tenant_id=paybond.harbor.tenant_id,verifier_context.verifier_id="paybond-gateway", and arequest_envelopeforPOST /harbor/intents/{intent_id}/fundwhose body digest is the SHA-256 of the exact JSON body Kit sends ({}for this helper). - Use a unique nonce, short
issued_at/expires_atwindow, and the Ed25519 private key for a tenant-registered agentkey_id; the signed proof includesmessage_digest_sha256_hex,signing_public_key_ed25519_hex, anded25519_signature_hex. - The retry needs its own recognition proof because proof nonces are single-use.
payment_signatureis separate: it comes from your x402 wallet or facilitator after signingpayment_required, and Kit sends it as thepayment-signatureheader. If a local quick-start script usesAPP_X402_PAYMENT_SIGNATURE, treat it as a stand-in for that signer output. paybond.intents.fundcalls GatewayPOST /harbor/intents/{id}/fund.402is a normal funding challenge, not an exception. The helper returnspayment_requiredso your integration can call an x402-capable signer.202means authorization is pending; poll/fundwith a fresh recognition proof before guarding tools.200means Harbor transitioned the intent tofunded.
Advanced troubleshooting
Only use direct token verification when support asks you to inspect an existing funded intent. Keep this out of production app configuration; normal integrations should read the capability token from create or fund.
export PAYBOND_INTENT_ID="00000000-0000-0000-0000-000000000000"
export PAYBOND_CAPABILITY="paybond_cap_..."
paybond = await Paybond.open(
api_key=os.environ["PAYBOND_API_KEY"],
expected_environment="sandbox",
)
try:
verified = await paybond.harbor.verify_capability(
intent_id=UUID(os.environ["PAYBOND_INTENT_ID"]),
token=os.environ["PAYBOND_CAPABILITY"],
operation="travel.booker.purchase",
requested_spend_cents=18_700,
)
print(verified.allow, verified.code, verified.message)
finally:
await paybond.aclose()
Settlement confirmation
Settlement confirmation is an operator-tier action and is not wrapped by paybond.intents. Prefer the Operator Console for human-reviewed settlement; for protocol-driven automation, use paybond.protocol.confirm_harbor_settlement(intent_id, body={}, recognition_proof=recognition_proof). The helper confirms the settlement action implied by the stored evidence evaluation. See the Harbor API reference.
Error handling
| Kit symbol | Raised when | Notes |
|---|---|---|
GatewayAuthError | Gateway rejected the API key or omitted tenant_id. | status_code and redacted body_text are attached to the instance. |
HarborHttpError | Any Harbor 4xx/5xx after retries are exhausted. | Log status_code, url, and a redacted body_text; never log the API key, JWT, or capability token. |
TenantBindingError | Harbor echoed a different tenant or intent than requested. | Treat as a critical tenant-isolation error; do not retry. |
See the Error handling guide for retry/backoff guidance and logging hygiene.
Further reading
- Python SDK reference — full surface, including
Paybond.open,paybond.intents.{create, fund, submit_evidence},paybond.harbor.verify_capability,paybond.aclose, and typed error classes. - Authentication & tenant binding.
- Evidence & artifacts and Capabilities.
- Agent integrations.
- Harbor API — the HTTP contract behind the SDK.