Lynx Capital: Autonomous Payouts
The Lynx Capital example is a complete Python application at examples/lynxCapital/ that runs a realistic weekly payout cycle: 4,200 invoices, 5 geographic regions, approximately $8.5M USD in payments, multi-currency settlement, regional tax compliance, vendor contract verification, and threshold-based routing across banking rails.
The application is not a tutorial scaffold. It is a production-pattern reference that demonstrates how Caracal’s authorization model maps to real enterprise workloads — specifically the patterns of hierarchical agent spawning, delegation scope enforcement, ephemeral single-operation agents, and multi-protocol transport across REST, gRPC, MCP, and SSE endpoints.
What this example demonstrates
Section titled “What this example demonstrates”Delegation hierarchy. The Finance Control agent holds global authority and delegates regional authority to Regional Orchestrator agents. Each Regional Orchestrator further delegates domain-scoped authority to agents that perform specific tasks (invoice extraction, ledger matching, compliance screening, payment execution). The hierarchy mirrors the organizational authority model that Caracal is designed to enforce.
Ephemeral payment agents. The Payment Execution agent is spawned for a single transaction: one vendor, one amount, one payment rail, one time window. It terminates immediately on completion. This is the pattern for operations that should have tightly bounded, non-transferable authority.
Exception agents. When anomalies are detected, an Exception agent is spawned with investigative-only scope. It can read vendor and compliance data but is explicitly not permitted to execute payments. The scope boundary is enforced at the authorization layer.
Multi-protocol transport. Within the same orchestration run, agents call external services over REST (Mercury Bank, Wise, NetSuite), gRPC (Treasury Operations), MCP (Vendor Portal), and SSE streaming (FX rates). All calls carry the agent’s identity and are traceable through the full event stream.
Full traceability. Every action — spawn, delegation, tool call, service call, result — emits a typed event. The complete lineage of who spawned whom, what tools they called, and what the external services returned is reconstructable from the event log.
Application layout
Section titled “Application layout”examples/lynxCapital/├── app/│ ├── main.py # FastAPI entry point; lifespan setup│ ├── config.py # Loads config/company.yaml│ ├── agents/│ │ ├── roles.py # 9 agent role definitions with scope templates│ │ ├── tools.py # 40+ tool functions (one per provider action)│ │ └── runner.py # AgentHandle, AgentRunner — session lifecycle│ ├── orchestration/│ │ ├── swarm.py # LLM orchestration loop (Finance Control + Regional)│ │ └── topology.py # Agent topology graph for visualization│ ├── services/│ │ ├── registry.py # Central dispatch: call(service_id, action, payload)│ │ └── transport/│ │ ├── rest.py # HTTP via httpx; retry, circuit breaker│ │ ├── grpc_client.py # gRPC unary calls with metadata auth│ │ ├── mcp.py # MCP tool invocation for vendor-portal│ │ └── sse.py # SSE streaming for FX rates│ ├── events/│ │ ├── bus.py # Event pub/sub for SSE streaming to UI│ │ └── types.py # Typed event factories│ └── api/│ ├── run.py # POST /api/start, GET /api/{id}/events│ └── ...├── config/│ └── company.yaml # Regions, providers, agent layers, system prompts├── _mock/│ ├── docker-compose.yml # 5 mock service containers│ ├── rest/ # Single FastAPI app fronting 13 REST providers│ ├── grpc/ # gRPC servers: treasury-ops (50051), compliance (50052)│ ├── mcp/ # MCP vendor portal (7800)│ └── streaming/ # SSE FX rates stream (8810)├── pyproject.toml└── .env.exampleRequirements
Section titled “Requirements”- Python 3.11+
- Docker and Docker Compose
- An OpenAI API key (
OPENAI_API_KEY) — the application fails at startup if this is absent
Install dependencies
Section titled “Install dependencies”cd examples/lynxCapitalpython -m venv .venvsource .venv/bin/activatepip install -e .Start mock providers
Section titled “Start mock providers”All 14 external providers run as deterministic mock services in Docker. Start them before the application:
docker compose -f _mock/docker-compose.yml up -d --buildThis starts:
| Container | Port | Provides |
|---|---|---|
rest | 8800 | 13 REST providers on a single port, routed by path |
fx-stream | 8810 | FX rate stream over SSE |
treasury-grpc | 50051 | Treasury operations over gRPC |
compliance-grpc | 50052 | Compliance streaming over gRPC |
vendor-mcp | 7800 | Vendor portal over MCP |
Configure environment
Section titled “Configure environment”cp .env.example .envEdit .env and set OPENAI_API_KEY. All other variables have defaults that point to the mock containers:
OPENAI_API_KEY=sk-...
# Mock service URLs (defaults shown — no change needed for local dev)LYNX_MERCURY_URL=http://127.0.0.1:8800LYNX_WISE_URL=http://127.0.0.1:8800LYNX_FX_STREAM_URL=http://127.0.0.1:8810/v1/streamLYNX_TREASURY_GRPC=127.0.0.1:50051LYNX_MCP_HOST=127.0.0.1LYNX_MCP_PORT=7800
# Dev credentials (pre-configured for mock services)LYNX_MERCURY_KEY=dev-mercury-bank-keyLYNX_WISE_KEY=dev-wise-payouts-key# ... (see .env.example for the full list)LYNX_MOCK_FAST=1 speeds up mock response latencies during development.
Start the application
Section titled “Start the application”uvicorn app.main:app --reload --port 8000Open http://localhost:8000. The landing page shows the scenario summary. Navigate to /demo to interact with the agent swarm.
Agent hierarchy
Section titled “Agent hierarchy”The swarm has 9 agent layers. Each layer is defined in app/agents/roles.py with a role name, scope template, and permitted tools.
Finance Control global scope└── Regional Orchestrator region:{region} ├── Invoice Intake invoice-batch:{region} ├── Ledger Match ledger-batch:{region} ├── Policy Check policy-batch:{region} ├── Route Optimization route-batch:{region} ├── Payment Execution * payment:{vendor_id}:{invoice_id} ├── Audit audit:{region} └── Exception * exception:{vendor_id}* — Ephemeral agents. They are spawned for a single operation and terminated on completion.
Scope templates determine tool authority. The Payment Execution agent’s scope is payment:{vendor_id}:{invoice_id} — scoped to exactly one vendor and one invoice. It is the only agent permitted to call submit_payment, submit_payout, and create_outbound_payment. No other agent has these tools. The Exception agent’s scope explicitly excludes all payment tools; it can only call check_vendor and get_vendor_profile.
Providers
Section titled “Providers”14 external providers are integrated, covering banking, ERP, compliance, document processing, FX, gRPC treasury operations, MCP vendor management, and regulatory filing.
| Provider | Category | Protocol | Actions |
|---|---|---|---|
mercury-bank | Banking | REST | get_account_balance, submit_payment |
wise-payouts | Payouts | REST | get_quote, submit_payout |
stripe-treasury | Banking | SDK (REST) | get_financial_account, create_outbound_payment |
netsuite | ERP | REST + async job | get_vendor_record, match_invoice |
sap-erp | ERP | REST + async job | get_vendor_record, match_invoice |
quickbooks | ERP | REST | get_vendor, match_bill |
compliance-nexus | Compliance | REST | check_vendor, check_transaction, get_withholding_rate, validate_tax_id |
ocr-vision | Document | REST + async job | extract_invoice |
vendor-portal | Vendor | MCP | get_vendor_profile, get_contract_terms, register_vendor |
tax-rules | Tax | SDK | get_withholding_rate, validate_tax_id |
fx-rates | FX | REST + SSE stream | get_rate, live rate stream |
treasury-ops | Treasury | gRPC | get_cash_position, forecast_liquidity, place_fx_hedge, transfer_funds |
close-engine | Accounting | REST + async job | post_journal_entry, reconcile_account, close_period |
regulatory-filings | Regulatory | REST + async job | aml_monitor_transaction, sanctions_screen_batch, prepare_regulatory_filing |
All providers route through the service registry at app/services/registry.py:
result = registry.call("mercury-bank", "submit_payment", { "vendor_id": "us-axiom-cloud", "amount": 50000, "currency": "USD", "rail": "ACH", "reference": "INV001",})The registry dispatches to the appropriate transport client (REST, gRPC, MCP, or SDK) based on the service ID.
Mock services
Section titled “Mock services”Every external provider call goes to a deterministic mock. Mock responses are defined in cases.json files keyed by a primary identifier (usually vendor_id):
{ "service": "mercury-bank", "actions": { "get_account_balance": { "match_key": "vendor_id", "cases": { "us-axiom-cloud": { "account_id": "acct-MCY-AXM-001", "balance": 487500.00, "currency": "USD", "status": "active" }, "default": { ... } } } }}The same input always produces the same output. This makes the example fully reproducible and testable without real credentials.
Payment submission mocks also emit webhooks with configurable delays, simulating the asynchronous settlement flow of real banking APIs:
# In _mock/rest/routers/mercury_bank.pydeliver("mercury-bank", "transaction.posted", {...}, delay_s=0.5)deliver("mercury-bank", "transaction.settled", {...}, delay_s=1.5)The application handles these callbacks at its webhook endpoint, updating payment status in the event stream.
Request flow
Section titled “Request flow”The following traces a complete execution from user input to completion.
1. User submits a prompt
Section titled “1. User submits a prompt”POST /api/startContent-Type: application/json
{"prompt": "Process US payouts for this week"}Response:
{"runId": "019500a2-..."}The application starts a background task and returns immediately. The client connects to the SSE stream to receive real-time events:
GET /api/019500a2-.../eventsAccept: text/event-stream2. Finance Control spawns and plans
Section titled “2. Finance Control spawns and plans”The swarm runner spawns the Finance Control agent with global scope. It receives the user prompt and builds a dispatch plan:
event: agent_spawndata: {"agent_id": "fc-001", "role": "finance-control", "scope": "global", "parent_id": null}
event: agent_startdata: {"agent_id": "fc-001"}
event: tool_calldata: {"agent_id": "fc-001", "tool": "dispatch_region", "args": {"region": "US", "focus": "Process payout cycle"}}
event: tool_resultdata: {"agent_id": "fc-001", "tool": "dispatch_region", "result": {"job_id": "job-us-001"}}Finance Control calls await_jobs(["job-us-001"]) and waits.
3. Regional Orchestrator executes
Section titled “3. Regional Orchestrator executes”A Regional Orchestrator agent is spawned as a child of Finance Control with region:US scope:
event: agent_spawndata: {"agent_id": "ro-us-001", "role": "regional-orchestrator", "scope": "region:US", "parent_id": "fc-001"}
event: delegationdata: {"from": "fc-001", "to": "ro-us-001", "scope": "region:US"}
event: agent_startdata: {"agent_id": "ro-us-001"}The Regional Orchestrator works through its invoice batch. For each invoice, it spawns the appropriate domain agent, awaits its result, and moves to the next step.
4. Invoice Intake processes a document
Section titled “4. Invoice Intake processes a document”event: agent_spawndata: {"agent_id": "ii-001", "role": "invoice-intake", "scope": "invoice-batch:US", "parent_id": "ro-us-001"}
event: tool_calldata: {"agent_id": "ii-001", "tool": "extract_invoice", "args": {"invoice_id": "INV001", "doc_id": "doc-INV001"}}
event: service_calldata: {"agent_id": "ii-001", "service": "ocr-vision", "action": "extract_invoice", "protocol": "REST"}
event: service_resultdata: {"agent_id": "ii-001", "service": "ocr-vision", "status": 200, "result": {"vendor": "Axiom Cloud", "amount": 50000, "currency": "USD"}}
event: tool_resultdata: {"agent_id": "ii-001", "tool": "extract_invoice", "result": {...}}
event: agent_terminatedata: {"agent_id": "ii-001", "status": "completed"}The Invoice Intake agent terminates immediately after returning its result. Its authority ends when the tool call completes.
5. Policy Check screens the vendor
Section titled “5. Policy Check screens the vendor”event: agent_spawndata: {"agent_id": "pc-001", "role": "policy-check", "scope": "policy-batch:US", "parent_id": "ro-us-001"}
event: service_calldata: {"service": "compliance-nexus", "action": "check_vendor", "args": {"vendor_id": "us-axiom-cloud"}}
event: service_resultdata: {"status": 200, "result": {"vendor_id": "us-axiom-cloud", "status": "cleared", "risk_score": 0.15}}
event: agent_terminatedata: {"agent_id": "pc-001", "status": "completed"}6. Payment Execution submits the payment
Section titled “6. Payment Execution submits the payment”The Payment Execution agent is spawned with scope payment:us-axiom-cloud:INV001. This is the narrowest possible authority: one vendor, one invoice.
event: agent_spawndata: {"agent_id": "pe-001", "role": "payment-execution", "scope": "payment:us-axiom-cloud:INV001", "parent_id": "ro-us-001"}
event: service_calldata: {"service": "mercury-bank", "action": "submit_payment", "args": {"vendor_id": "us-axiom-cloud", "amount": 50000, "currency": "USD", "rail": "ACH", "reference": "INV001"}}
event: service_resultdata: {"status": 200, "result": {"tx_id": "TX-MCY-AXM-001", "status": "submitted"}}
event: agent_terminatedata: {"agent_id": "pe-001", "status": "completed"}Mercury Bank enqueues two webhook callbacks — transaction.posted (0.5 seconds) and transaction.settled (1.5 seconds) — which arrive asynchronously and update the event stream.
7. Finance Control receives the result
Section titled “7. Finance Control receives the result”When the Regional Orchestrator finishes all invoices, its job completes and Finance Control’s await_jobs resolves:
event: agent_terminatedata: {"agent_id": "ro-us-001", "status": "completed"}
event: tool_resultdata: {"agent_id": "fc-001", "tool": "await_jobs", "result": {"status": "completed", "summary": {...}}}
event: agent_terminatedata: {"agent_id": "fc-001", "status": "completed"}
event: run_enddata: {"run_id": "019500a2-...", "status": "completed"}Multi-protocol transport
Section titled “Multi-protocol transport”The same orchestration run calls providers over four different protocols. The transport client is selected by the service registry based on the service ID:
REST (Mercury Bank, Wise, NetSuite, etc.):
client = RestClient(base_url="http://127.0.0.1:8800", auth=AuthSpec("Bearer", key))response = await client.post("/v1/payments", payload)Includes retry logic with exponential backoff, circuit breaker, and idempotency key support for write operations.
gRPC (Treasury Operations):
client = GrpcClient("127.0.0.1:50051", metadata={"authorization": f"Bearer {token}"})response = await client.call("GetCashPosition", request)MCP (Vendor Portal):
client = McpClient(host="127.0.0.1", port=7800)result = await client.call_tool("vendor.get_profile", {"vendor_id": "us-axiom-cloud"})SSE streaming (FX rates):
async for rate_update in sse_stream("http://127.0.0.1:8810/v1/stream"): update_cached_rate(rate_update["pair"], rate_update["rate"])Observing a run
Section titled “Observing a run”Live event stream
Section titled “Live event stream”All events are available over SSE at /api/{run_id}/events. The event types in emission order:
| Event type | Description |
|---|---|
agent_spawn | Agent created; includes role, scope, parent ID |
delegation | Authority delegated from parent to child |
agent_start | Agent begins executing |
tool_call | Agent invokes a tool; includes tool name and args |
service_call | Tool dispatches to an external service; includes protocol |
service_result | External service responds; includes HTTP/gRPC status |
tool_result | Tool returns to agent |
agent_end | Agent signals completion |
agent_terminate | Agent session cleaned up; includes final status |
run_end | Run complete |
Agent lineage
Section titled “Agent lineage”GET /api/{run_id}/lineageReturns the complete agent spawn tree with delegation edges, timing, and per-agent status. Use this to verify the hierarchy formed correctly and to trace which agent called which service.
GET /api/{run_id}/events streams structured logs alongside agent events. The web UI at /logs shows them color-coded by category: service calls in teal, agent actions in blue, errors in red.
Resilience behavior
Section titled “Resilience behavior”Tool retries. Each tool call retries up to 3 times on transient errors before propagating the failure to the agent.
Circuit breaker. app/services/resilience.py tracks error rates per provider. A provider that fails repeatedly trips its circuit breaker, causing subsequent calls to fail immediately rather than waiting for timeouts. This prevents one degraded provider from stalling the entire run.
Partial completion. A failed invoice does not abort the entire regional batch. The Regional Orchestrator continues processing remaining invoices and reports failures in its result. Finance Control aggregates partial results across regions.
Cancellation. POST /api/{run_id}/cancel triggers runner.cancel_subtree(root_agent_id), which terminates all running agents in the tree from the bottom up.
Running the tests
Section titled “Running the tests”pytest tests/ -vTests cover agent lifecycle behavior, topology graph correctness, and provider transport logic. They run against the same mock services and do not require a live Caracal stack.
Adapting this example
Section titled “Adapting this example”The Lynx Capital application demonstrates the authorization patterns that Caracal enforces:
To add a new agent layer: Define a role in app/agents/roles.py with a scope template and permitted tools. Add the spawn logic to the orchestrator that should create it. The scope template becomes the Caracal scope claim when delegation is used.
To add a new provider: Add the provider to config/company.yaml, create a cases.json in _mock/, add a router in _mock/rest/routers/, and register the dispatch handler in app/services/registry.py.
To wire in the Caracal SDK: The authorization patterns in this example — spawn, delegate, scoped mandates, transport headers — map directly to Caracal.from_env(), caracal.spawn(), caracal.delegate(), and caracal.transport() in the Python SDK. The application’s AgentRunner.spawn() and event delegation edges are the conceptual equivalents. Replace them with the SDK to get cryptographic enforcement of scope boundaries instead of application-level simulation.