Architecture: Providers as Tools. End-to-End
Core insight: A Revolut card payment is the same thing as a Gmail send or a Slack message. It’s a tool call. the LLM says “do this”, and TapPass governs whether it happens, with what constraints, and audits the result.
The Problem With Treating Cards as Special
Section titled “The Problem With Treating Cards as Special”If we build “Revolut card governance” as a special subsystem, we’ll end up with:
- A
card_spending_policypipeline step (special-cased) - A
spending_pactextension (special-cased) - A
revolut_freezeauto-action (special-cased) - A whole “payments” module that lives outside the existing architecture
This doesn’t scale. Next month it’s Stripe issuing, then Wise transfers, then Brex cards. Each one gets its own governance module.
The existing architecture already solves this. A Revolut card payment is just:
tool_name: "revolut_card_payment"operation: "write"params: { amount: 4999, currency: "EUR", merchant_mcc: "5734" }provider: revolut (config/providers/revolut.yaml)adapter: GenericHTTPAdaptercredential: vault (OAuth2 bearer token)It flows through the exact same pipeline as gmail_send or slack_send_message. The governance comes from configuring the existing steps, not building new ones.
Current Architecture (Already Works)
Section titled “Current Architecture (Already Works)”LLM says: "call send_email(to='alice@acme.com', subject='Q3 Report')" │ ▼┌──────────────────────────────────────────────────────────────┐│ TapPass Pipeline (same for ALL tool calls) ││ ││ BEFORE: ││ validate_input → well-formed? ││ detect_pii → PII in params? ││ budget_enforcement → within monthly budget? ││ session_budget → within session budget? ││ ││ CALL: ││ tool_permissions → is send_email allowed? ││ tool_constraints → to=*@acme.com? subject≠*confid*? ││ user_tool_scopes → can this user send email? ││ require_approval → amount > threshold → human gate ││ call_tool → vault credential → HTTP call ││ ││ AFTER: ││ scan_output → response safe? ││ cost_tracking → record cost ││ audit_trail → hash-chained log │└──────────────────────────────────────────────────────────────┘This is identical for a Revolut payment:
LLM says: "call revolut_card_payment(amount=4999, currency='EUR', description='AWS credits')" │ ▼┌──────────────────────────────────────────────────────────────┐│ TapPass Pipeline (same pipeline, same steps) ││ ││ BEFORE: ││ validate_input → well-formed? ││ detect_pii → PII in params? (card data is PII) ││ budget_enforcement → within monthly spending budget? ││ session_budget → within session spending cap? ││ ││ CALL: ││ tool_permissions → is revolut_card_payment allowed? ││ tool_constraints → amount≤10000? currency∈[EUR,USD]? ││ merchant_mcc∈[5734,4816]? ││ user_tool_scopes → can this agent use this card? ││ require_approval → amount > 50000 → human gate ││ call_tool → vault credential → Revolut API ││ ││ AFTER: ││ scan_output → response safe? ││ cost_tracking → record spending ││ audit_trail → hash-chained log │└──────────────────────────────────────────────────────────────┘Nothing new is needed. The governance is configured, not coded.
What a Provider Actually Is
Section titled “What a Provider Actually Is”Looking at the existing code, a provider is defined by three layers:
Layer 1: YAML Spec (config/providers/{name}.yaml)
Section titled “Layer 1: YAML Spec (config/providers/{name}.yaml)”Declares the API surface. endpoints, auth, operations, constraints, GDPR metadata:
name: revolutdisplay_name: "Revolut Business"api_base_url: https://b2b.revolut.com/api/1.0
auth: type: bearer header: Authorization prefix: "Bearer"
oauth: client_id_env: REVOLUT_CLIENT_ID client_secret_env: REVOLUT_CLIENT_SECRET authorize_url: https://business.revolut.com/app-confirm token_url: https://b2b.revolut.com/api/1.0/auth/token scopes: [cards:read, cards:write, transactions:read, payments:write]
operations: # ── Card Operations ──────────────────────────────── - name: create_card op_group: write method: POST endpoint: /cards description: Issue a virtual card for an agent constraints: - name: allowed_labels type: string_list param_field: label description: Card label must match an allowed pattern
- name: freeze_card op_group: write method: POST endpoint: /cards/{card_id}/freeze description: Freeze a card instantly
- name: unfreeze_card op_group: write method: POST endpoint: /cards/{card_id}/unfreeze description: Unfreeze a card
- name: update_card_limits op_group: write method: PATCH endpoint: /cards/{card_id} description: Update spending limits on a card constraints: - name: max_single_amount type: int param_field: spending_limits.single.amount description: Maximum per-transaction limit that can be set
# ── Transaction Queries ──────────────────────────── - name: list_transactions op_group: read method: GET endpoint: /transactions description: List card transactions
- name: get_transaction op_group: read method: GET endpoint: /transactions/{txn_id} description: Get transaction details
# ── Payments ─────────────────────────────────────── - name: create_payment op_group: write method: POST endpoint: /pay description: Create a bank transfer constraints: - name: max_amount type: int param_field: amount description: Maximum payment amount (in minor units) - name: allowed_currencies type: item_list param_field: currency description: Allowed payment currencies - name: allowed_accounts type: item_list param_field: receiver.account_id description: Allowed recipient accounts
adapter_class: tappass.vault.providers.generic.GenericHTTPAdapter
data_region: eueu_compliant: truegdpr_role: processorgdpr_legal_basis: contractgdpr_transfer_mechanism: eu_onlydpa_reference: "https://www.revolut.com/legal/business-data-processing"data_categories: [transactions, card_details, payment_data]retention_days: 2555 # 7 years (financial records)Layer 2: Adapter (GenericHTTPAdapter. already exists)
Section titled “Layer 2: Adapter (GenericHTTPAdapter. already exists)”The GenericHTTPAdapter reads the YAML spec and proxies HTTP calls. No new code needed for Revolut. it’s just REST + Bearer auth, same as Holded, Slack, Jira.
The adapter handles:
- Path param substitution (
/cards/{card_id}→/cards/card_abc123) - Credential injection from vault (Bearer token in Authorization header)
- Method routing (GET → query params, POST → JSON body)
- Error handling and response parsing
Layer 3: Pipeline Config (per-agent governance)
Section titled “Layer 3: Pipeline Config (per-agent governance)”The CISO configures the pipeline for agents that use Revolut. This is the same pipeline config mechanism used for every other tool:
# Pipeline for "procurement-agent". assigned via admin APIcategories: before: access_control: steps: validate_input: {} rate_limit: { max_calls: 100, window_seconds: 3600 } budget_enforcement: { max_usd_monthly: 5000 } session_budget: { max_cost_usd: 500 } data_protection: steps: detect_pii: {} detect_secrets: {} call: route_and_execute: steps: tool_permissions: permissions: revolut_create_payment: block # no bank transfers revolut_create_card: block # no card issuance revolut_freeze_card: allow # can freeze (safety) revolut_unfreeze_card: block # can't unfreeze revolut_list_transactions: allow # can read revolut_get_transaction: allow # can read revolut_update_card_limits: block # can't change limits
tool_constraints__revolut_create_payment: on_detection: block constraints: revolut_create_payment: amount: { max: 50000 } # €500 max currency: { include: ["EUR", "USD", "GBP"] } "receiver.account_id": { include: ["acc_supplier1", "acc_supplier2"] }
require_approval: on_detection: block actions: [revolut_create_payment] timeout_seconds: 300
call_tool: { timeout: 30 } after: tracking: steps: cost_tracking: {}The Pattern: Every Integration is a Tool
Section titled “The Pattern: Every Integration is a Tool”┌─────────────────────────────────────────────────────────────────────┐│ TOOL = PROVIDER OPERATION ││ ││ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ││ │ gmail_send │ │ slack_send │ │ revolut_pay │ ... ││ │ │ │ _message │ │ │ ││ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ ││ │ │ │ ││ ▼ ▼ ▼ ││ ┌──────────────────────────────────────────────────────┐ ││ │ YAML SPEC (config/providers/) │ ││ │ defines: endpoints, auth, constraints, GDPR │ ││ └──────────────────────────────────────────────────────┘ ││ │ │ │ ││ ▼ ▼ ▼ ││ ┌──────────────────────────────────────────────────────┐ ││ │ ADAPTER (GenericHTTPAdapter) │ ││ │ handles: HTTP call, credential injection, errors │ ││ └──────────────────────────────────────────────────────┘ ││ │ │ │ ││ ▼ ▼ ▼ ││ ┌──────────────────────────────────────────────────────┐ ││ │ VAULT (credential storage) │ ││ │ handles: OAuth tokens, API keys, encrypted storage │ ││ └──────────────────────────────────────────────────────┘ ││ │ │ │ ││ ▼ ▼ ▼ ││ ┌──────────────────────────────────────────────────────┐ ││ │ PIPELINE (same steps for all tools) │ ││ │ tool_permissions → is this tool allowed? │ ││ │ tool_constraints → are params within bounds? │ ││ │ user_tool_scopes → can this user call this? │ ││ │ require_approval → human-in-the-loop? │ ││ │ budget_enforcement → within spending limit? │ ││ │ call_tool → execute via adapter │ ││ └──────────────────────────────────────────────────────┘ ││ │ │ │ ││ ▼ ▼ ▼ ││ ┌──────────────────────────────────────────────────────┐ ││ │ CAPABILITY TOKEN │ ││ │ tools: ["gmail_send"] tools: ["revolut_pay"] │ ││ │ constraints: {to: ...} constraints: {amount: ...} │ ││ │ ops: ["write"] ops: ["write"] │ ││ └──────────────────────────────────────────────────────┘ ││ │ │ │ ││ ▼ ▼ ▼ ││ ┌──────────────────────────────────────────────────────┐ ││ │ AUDIT TRAIL (hash-chained, immutable) │ ││ │ same format for every tool call │ ││ └──────────────────────────────────────────────────────┘ │└─────────────────────────────────────────────────────────────────────┘What “Governed Spending” Actually Means in This Architecture
Section titled “What “Governed Spending” Actually Means in This Architecture”There’s no “spending governance module.” There are existing pipeline steps configured for financial tools:
| Existing Step | Config for Financial Tools |
|---|---|
budget_enforcement | max_usd_monthly: 5000: same step that governs LLM token spend, now also governs card spend |
session_budget | max_cost_usd: 500: cap a single agentic session’s total spend |
tool_permissions | revolut_create_payment: block or allow: per-tool on/off |
tool_constraints | amount: {max: 50000}, currency: {include: ["EUR"]}, merchant_mcc: {include: ["5734"]} |
user_tool_scopes | scope: "own": agent can only use its own card, not others |
require_approval | actions: [revolut_create_payment]: human gate for payments |
scan_tool_calls | Already blocks dangerous patterns in all tool calls |
cost_tracking | Records the payment amount: same cost tracking as LLM calls |
The capability token naturally carries the constraints:
{ "tools": ["revolut_create_payment"], "ops": ["write"], "cstr": { "revolut_create_payment": { "amount": { "type": "range", "max": 50000 }, "currency": { "type": "one_of", "values": ["EUR", "USD"] } } }, "cls": "INTERNAL", "health": 88.9, "compliance": "regulated"}What’s Actually Needed to Add Revolut
Section titled “What’s Actually Needed to Add Revolut”1. One YAML file: config/providers/revolut.yaml
Section titled “1. One YAML file: config/providers/revolut.yaml”Declares the Revolut Business API surface. ~60 lines. Uses GenericHTTPAdapter: no custom Python code.
2. One tool catalog entry: tappass/tool_catalog.yaml
Section titled “2. One tool catalog entry: tappass/tool_catalog.yaml”revolut: display_name: Revolut Business risk: high category: payments default: consent tools: - tappass: revolut.create_card - tappass: revolut.freeze_card - tappass: revolut.list_transactions - tappass: revolut.create_payment3. Pipeline configuration (CISO does this, no code)
Section titled “3. Pipeline configuration (CISO does this, no code)”The CISO assigns a pipeline to the agent via the dashboard or admin API:
tp.create_pipeline("financial-agent", steps={ "budget_enforcement": {"max_usd_monthly": 5000}, "tool_permissions": { "permissions": { "revolut_create_payment": "allow", "revolut_create_card": "block", } }, "tool_constraints__revolut": { "on_detection": "block", "constraints": { "revolut_create_payment": { "amount": {"max": 50000}, "currency": {"include": ["EUR", "USD"]}, } } }, "require_approval": { "actions": ["revolut_create_payment"], "timeout_seconds": 300, },})tp.assign("procurement-agent", "financial-agent")4. OAuth Connect (existing flow)
Section titled “4. OAuth Connect (existing flow)”Employee visits /connect/revolut → Revolut consent screen → token stored in vault. Already built.
5. Behavioral Pact (existing feature)
Section titled “5. Behavioral Pact (existing feature)”curl -X PUT localhost:9620/agents/procurement-agent/pact \ -d '{ "purpose": "Purchase software licenses and cloud credits", "expected_classification": "INTERNAL", "intended_tools": ["revolut_create_payment", "revolut_list_transactions"], "intended_operations": ["read", "write"], "expected_monthly_budget_usd": 5000, "expected_cost_per_call_usd": 100 }'What Emerges From This Design
Section titled “What Emerges From This Design”Because everything is a tool, you get cross-tool governance for free:
Pact adherence across tool types
Section titled “Pact adherence across tool types”Pact says: "intended_tools: [revolut_list_transactions]" (read-only)Agent calls: revolut_create_payment→ Pact violation detected automatically (tool not in intended_tools)→ Health score drops, drift flaggedCross-tool budget enforcement
Section titled “Cross-tool budget enforcement”Agent spends $200 on OpenAI tokens (LLM calls) +Agent spends $300 on Revolut payment +Agent spends $100 on Slack premium API calls= $600 total. all tracked by the same budget_enforcement stepDrift detection across tool types
Section titled “Drift detection across tool types”Agent usually: 90% revolut_list_transactions, 10% revolut_create_paymentAgent today: 20% list, 80% create_payment→ Tool usage drift: MAJOR (Jaccard distance)→ Alert fired to CISOTrust scoring affects all tools equally
Section titled “Trust scoring affects all tools equally”Trust score drops to 400 (agent caused incidents)→ OPA policy: trust < 600 → tool_constraints.amount.max = 5000 (€50)→ Applies to revolut_create_payment AND any other financial tool→ No special code. OPA evaluates trust for every tool callHuman approval is tool-agnostic
Section titled “Human approval is tool-agnostic”require_approval: actions: - revolut_create_payment # payment > threshold → approve - gmail_send # email to external → approve - holded_create_invoice # new invoice → approve - jira_delete_issue # destructive action → approveSame step, same flow, same Slack webhook, same admin API to approve/deny.
Reactive Governance: Webhooks as Inbound Tool Calls
Section titled “Reactive Governance: Webhooks as Inbound Tool Calls”Revolut sends transaction webhooks. These are the reverse of a tool call. the external service tells TapPass something happened. But the pipeline still governs the response:
Revolut webhook: TransactionCreated { card_id, amount: 25000, mcc: 7995 } │ ▼TapPass receives webhook │ ├─ Record in audit trail (hash-chained) ├─ Check against pact: MCC 7995 (gambling) not in allowed_mccs → violation ├─ Check against budget: $250 within limits but unexpected category ├─ Drift detection: new MCC → MAJOR drift signal │ ├─ Auto-response (governed by OPA policy): │ └─ drift_level == "major" → freeze_card (call back to Revolut API) │ └─ Alert CISO via webhookThis fits the existing webhook alerting system (observability/alerting.py). The only new piece is a webhook receiver endpoint. a standard FastAPI route that feeds events into the audit trail and optionally triggers tool calls (like freeze_card) through the normal pipeline.
Webhook Receiver (minimal new code)
Section titled “Webhook Receiver (minimal new code)”@router.post("/webhooks/revolut")async def revolut_webhook(request: Request): payload = await request.json() event_type = payload.get("event")
# Record in audit trail await app.audit.record(AuditEvent( event_type=f"revolut_{event_type}", resource="revolut", details=payload, ))
# Check policy. should we auto-respond? if event_type == "TransactionCreated": txn = payload.get("data", {}) # Feed through pipeline as a "reactive tool call" # The pipeline decides whether to freeze, alert, or ignore ...Adding the Next Provider
Section titled “Adding the Next Provider”This architecture means adding Stripe Issuing, Wise, Brex, or any other financial service is:
- One YAML file (
config/providers/stripe_issuing.yaml) - One catalog entry (5 lines in
tool_catalog.yaml) - Pipeline config (CISO does this, no code)
name: wisedisplay_name: "Wise Business"api_base_url: https://api.transferwise.com/v3auth: type: bearer header: Authorization prefix: "Bearer"operations: - name: create_transfer op_group: write method: POST endpoint: /profiles/{profile_id}/transfers constraints: - name: max_amount type: int param_field: targetAmount - name: allowed_currencies type: item_list param_field: targetCurrencyadapter_class: tappass.vault.providers.generic.GenericHTTPAdapterSame pipeline governs it. Same capability tokens authorize it. Same audit trail logs it. Same dashboard shows it.
Summary
Section titled “Summary”Don’t build a payments module. Configure the existing tool governance for payment tools.
| What | How | New Code? |
|---|---|---|
| Revolut API access | YAML provider spec + GenericHTTPAdapter | One YAML file |
| Spending limits | tool_constraints step (amount: max) | No: config only |
| MCC restrictions | tool_constraints step (mcc: include) | No: config only |
| Per-agent card access | tool_permissions + user_tool_scopes | No: config only |
| Human approval | require_approval step | No: config only |
| Budget caps | budget_enforcement + session_budget | No: config only |
| Credential storage | Vault + OAuth Connect | No: existing flow |
| Capability tokens | Token minting with constraints | No: automatic |
| Audit trail | Same hash-chained audit | No: automatic |
| Behavioral pacts | Same pact system | No: config only |
| Trust-based limits | OPA policy + trust scoring | No: OPA config |
| Drift detection | Same drift engine | No: automatic |
| Webhook reactions | One webhook endpoint | ~50 lines |
| Dashboard visibility | Tools already show in dashboard | No: automatic |
Total new code: One YAML file + one webhook endpoint. Everything else is configuration of existing systems.
The architecture is: providers are tools, tools are governed by the pipeline, the pipeline is configured by the CISO. Adding a new financial provider is the same effort as adding a new SaaS integration: because it is the same thing.