← Back to list

Day 71 1.3: Agentic AI in Open Banking: Building on Open Data Contracts(semantic guarantees)

Agentic AI in Open Banking: Building on Open Data Contracts

LAKSHMI VENKATESH in AI Smart · 2026-05-11 05:35 · 50 claps · 7.5 min read
#odc #smart-contracts #ai #artificial-intelligence #ai-agent
Open on Medium ↗
Wiki topics: AGT · AI Agents AI · AI · General CRY · Crypto & Web3 FIN · Fintech & Banking ECO · Economy · General

Day 71 1.3: Agentic AI in Open Banking: Building on Open Data Contracts(semantic guarantees)

Agentic AI in Open Banking: Building on Open Data Contracts

The architecture that makes agents trustworthy at ecosystem scale

Executive Summary

Open Banking created standardized APIs. Agentic AI creates autonomous intelligence. Open Data Contracts make that intelligence trustworthy.

This article explains why Open Data Contracts — inspired by ODCS (Open Data Contract Standard) — are the missing semantic layer between Open Banking APIs and agentic systems, and how to build the complete architecture from knowledge graphs to zero-shot predictions to MCP-federated execution.

What we will discuss:

  • Why Open Banking APIs need semantic contracts (not just schemas)
  • How ODCS principles translate to financial services
  • The complete architecture: Contracts → Knowledge Graph → Zero-Shot ML → MCP

  • Implementation patterns with real examples
  • The regulatory and audit advantages

Part 1: The Problem — APIs Without Semantics

What Open Banking Gave Us

Open Banking (PSD2 in Europe, CMA in UK, similar frameworks globally) mandated that banks expose standardized APIs for:

  • Account information (/accounts/{id}/balance, /accounts/{id}/transactions)
  • Payment initiation (/payments/domestic, /payments/international)
  • Confirmation of funds (/funds-confirmations)

The promise: Third parties can build services on top of a unified financial data layer.

The reality: Each bank’s API returns different semantics under the same endpoint.

The Semantic Gap:

Questions an agent can’t answer:

  • Is balance: 5000 in pounds or pence? (£50 vs £5,000)
  • Is this available balance or ledger balance?
  • How fresh is this data? (Real-time or cached?)
  • What’s the data source? (Core banking ledger, cache, warehouse?)
  • What happens if this data is wrong?
  • Can I rely on this for a £10K transfer decision?

At human speed: A developer catches this in testing, hardcodes exceptions per bank.

At machine speed: An agent executes 1,000 transactions on malformed data before anyone notices.

Why JSON Schema Isn’t Enough

JSON Schema validates structure, not meaning:

# JSON Schema tells you:
balance:
  type: integer

# JSON Schema does NOT tell you:
- Units: minor (pence) or major (pounds)?
- Semantics: available, ledger, or pending?
- Freshness: real-time or cached?
- Source: which system?
- SLA: what uptime guarantee?

For human developers building CRUD apps, this is manageable.

For agentic AI making autonomous financial decisions, this is catastrophic.

Part 2: The Solution — Open Data Contracts

The Open Data Contracts Structure

What Are Open Data Contracts?

Open Data Contract Standard (ODCS) is a specification for machine-readable data contracts that declare:

  • Schema — Shape of the data (JSON Schema)
  • Semantics — Meaning of the data (business logic)
  • Quality — SLA, accuracy, completeness
  • Lineage — Source system, ownership, refresh rate
  • Compliance — Regulatory standards met

Originally designed for: Data lakes, analytics pipelines, data governance.

We’re adapting it for: Open Banking APIs consumed by agentic AI systems.

ODCS Principles Applied to Open Banking

Example: Open Data Contract for /accounts/balance

# Open Banking API Contract v2.0
contract_id: "account_balance_v2"
version: "2.0.0"
published_date: "2024-06-15T00:00:00Z"
status: "active"

publisher:
  organization: "Retail Banking Division"
  team: "Treasury Operations"
  contact: "api-support@example.com"

api_endpoint:
  path: "/accounts/{accountId}/balance"
  method: "GET"
  base_url: "https://api.openbanking.example.com/v2"

schema:
  response:
    type: "object"
    properties:
      balance:
        type: "integer"
        description: "Account balance in minor currency units"
        example: 500000

      currency:
        type: "string"
        format: "ISO_4217"
        description: "Three-letter currency code"
        example: "GBP"

      balanceType:
        type: "string"
        enum: ["available", "ledger", "pending"]
        description: "Type of balance returned"
        example: "available"

      lastUpdated:
        type: "string"
        format: "date-time"
        description: "Timestamp of last balance update"

    required: ["balance", "currency", "balanceType", "lastUpdated"]

semantics:
  balance_units: "minor_currency"  # Always pence/cents
  balance_calculation: "Real-time query to core banking ledger"
  includes_pending_transactions: false
  available_vs_ledger: "available = ledger - holds - pending debits"

quality:
  sla:
    uptime: "99.9%"
    p95_latency_ms: 200
    p99_latency_ms: 500

  accuracy:
    tolerance: "£0.01"
    reconciliation_frequency: "real-time"

  rate_limits:
    per_customer: "100 requests/minute"
    per_application: "10000 requests/minute"

lineage:
  source_system: "Core Banking Ledger v3.2"
  source_table: "accounts.current_balance"
  refresh_mechanism: "Event-driven (transaction posting)"
  data_owner: "Treasury Operations"
  steward: "Head of Digital Banking"

  data_flow:
    - system: "Core Banking"
      update_frequency: "real-time"
    - system: "API Gateway Cache"
      ttl_seconds: 5

compliance:
  regulations:
    - name: "PSD2"
      article: "Article 36 - Access to payment account"
      jurisdiction: "EU"

    - name: "GDPR"
      article: "Article 6(1)(b) - Contract"
      lawful_basis: "Customer consent via OAuth 2.0"

    - name: "UK CMA Open Banking"
      version: "3.1.10"
      specification_url: "https://standards.openbanking.org.uk/"

  data_classification: "PII - Personal Financial Information"
  retention_policy: "7 years per financial regulation"

changelog:
  - version: "2.0.0"
    date: "2024-06-15"
    changes:
      - "Added balanceType field to distinguish available vs ledger"
      - "Added lastUpdated timestamp"
      - "Changed balance to always be minor units (breaking change)"
    breaking: true
    migration_guide: "https://docs.example.com/migration-v2"

  - version: "1.0.0"
    date: "2023-01-10"
    changes:
      - "Initial release"
    breaking: false

What This Contract Guarantees

For Agents:

  • balance: 500000 = £5,000.00 (guaranteed minor units)
  • balanceType: "available" = ledger minus holds/pending
  • Data is ≤5 seconds old (cache TTL)
  • Source is real-time ledger, not warehouse
  • 99.9% uptime SLA
  • <200ms response time (p95)

For Regulators:

  • PSD2 Article 36 compliant
  • GDPR lawful basis: customer consent
  • UK CMA Open Banking 3.1.10 specification
  • Full lineage from core banking to API
  • 7-year retention as required

For Governance:

  • Data owner: Treasury Operations
  • Breaking changes tracked with migration guides
  • Version 2.0.0 published 2024–06–15
  • Contact: api-support@example.com

Part 3: Architecture Layer 1 — Contract-Validated Middleware

Where Contracts Fit in the Stack

Contract Validation Flow

Step 1: Agent makes API call via MCP

# Agent calls MCP tool
result = mcp.call("banking.get_balance", {
    "account_id": "ACC123"
})

Step 2: Middleware intercepts

# Middleware loads published contract
contract = load_contract("account_balance_v2")

# Makes actual API call
response = http.get(f"{bank_api_url}/accounts/ACC123/balance")

# Validates response against contract
validation_result = validate_response(response, contract)

if not validation_result.valid:
    # Circuit breaker fires
    log_violation(contract, response, validation_result.errors)
    raise ContractViolationError(
        f"API response violated contract: {validation_result.errors}"
    )

Step 3: Semantic validation

# Schema validation (JSON Schema)
if response.balance is not int:
    raise SchemaViolation("balance must be integer")

# Semantic validation (contract guarantees)
if response.balance_units != "minor_currency":
    raise SemanticViolation("balance must be in minor units per contract")

# SLA validation
if response_time_ms > contract.quality.sla.p95_latency_ms:
    log_sla_breach(contract, response_time_ms)

# Freshness validation
age_seconds = now() - response.lastUpdated
if age_seconds > contract.lineage.cache_ttl:
    raise FreshnessViolation(f"Data is {age_seconds}s old, max allowed: 5s")

Step 4: Return validated data to agent

# Agent receives data with contract guarantees
# Can now trust:
# - balance is in pence
# - balanceType tells available vs ledger
# - data is ≤5s old
# - source is real-time ledger

Contract Repository Architecture

Centralized contract registry:

/contracts
  /banking
    /accounts
      - balance_v1.yaml
      - balance_v2.yaml (current)
      - transactions_v1.yaml
    /payments
      - domestic_v1.yaml
      - international_v1.yaml
  /wealth
    /portfolio
      - holdings_v1.yaml
      - performance_v1.yaml

Contract discovery:

# Agent asks: "What contracts are available for accounts?"
contracts = registry.list_contracts(domain="banking.accounts")

# Returns:
[
  {
    "contract_id": "account_balance_v2",
    "version": "2.0.0",
    "endpoint": "/accounts/{id}/balance",
    "status": "active"
  },
  {
    "contract_id": "account_transactions_v1",
    "version": "1.0.0",
    "endpoint": "/accounts/{id}/transactions",
    "status": "active"
  }
]

Middleware loads contract on first call, caches:

# First call: load and cache
contract = registry.get_contract("account_balance_v2")
cache.set(f"contract:{contract_id}", contract, ttl=3600)

# Subsequent calls: use cached contract
contract = cache.get(f"contract:{contract_id}")

Part 4: Architecture Layer 2 — Knowledge Graph from Contracts

Why Agents Need a Graph, Not Just APIs

The problem with API-first:

Agent needs: "All accounts belonging to Customer X"

Without graph:
1. Call /customers/X → get customer details
2. Parse response, find account_ids list
3. For each account_id:
   4. Call /accounts/{id}/balance
   5. Call /accounts/{id}/details

Result: N+1 queries, slow, no relationships

With Knowledge Graph:

Agent queries graph:
MATCH (c:Customer {id: "X"})-[:OWNS]->(a:Account)
RETURN a

Result: One query, instant, includes all relationships

Building the Graph from Contracts

Contracts declare entities and relationships:

# Contract: account_balance_v2.yaml
entities:
  Account:
    identifier: "accountId"
    properties:
      - balance (integer, minor units)
      - currency (ISO 4217)
      - balanceType (enum)

    relationships:
      - name: "ownedBy"
        target: "Customer"
        cardinality: "many-to-one"
        inverse: "owns"

      - name: "partOf"
        target: "Portfolio"
        cardinality: "many-to-many"
        description: "Accounts can belong to investment portfolios"

      - name: "linkedTo"
        target: "ExternalAccount"
        cardinality: "many-to-many"
        description: "Accounts linked via Open Banking consent"

Graph ingestion pipeline:

# 1. Read all published contracts
contracts = registry.list_all_contracts()

# 2. Extract entity schemas
for contract in contracts:
    for entity_name, entity_schema in contract.entities.items():
        graph.create_node_type(
            name=entity_name,
            properties=entity_schema.properties,
            identifier=entity_schema.identifier
        )

# 3. Extract relationships
for contract in contracts:
    for entity in contract.entities:
        for relationship in entity.relationships:
            graph.create_edge_type(
                name=relationship.name,
                source=entity.name,
                target=relationship.target,
                cardinality=relationship.cardinality
            )

# 4. Populate graph from APIs
for customer in all_customers():
    # Create customer node
    graph.create_node("Customer", customer.id, customer.data)

    # Get accounts via API
    accounts = api.get_accounts(customer.id)

    for account in accounts:
        # Create account node
        graph.create_node("Account", account.id, account.data)

        # Create relationship
        graph.create_edge("Customer", customer.id, "owns", "Account", account.id)

Multi-Bank Graph Unification

The challenge: Customer has accounts at multiple banks.

Without contracts:

Internal Bank: balance field = minor units (pence)
External Bank A: balance field = major units (pounds)
External Bank B: accountBalance.amount = string "50.00"

Graph contains semantically inconsistent data

With contracts:

# Load contracts for all banks
internal_contract = load_contract("internal", "account_balance_v2")
external_a_contract = load_contract("bank_a", "account_balance_v1")
external_b_contract = load_contract("bank_b", "account_balance_v2")

# Normalize based on contract semantics
def normalize_balance(bank, raw_response):
    contract = load_contract(bank, "account_balance")

    if contract.semantics.balance_units == "minor_currency":
        # Already in pence
        return raw_response.balance

    elif contract.semantics.balance_units == "major_currency":
        # Convert pounds to pence
        return int(raw_response.balance * 100)

    elif contract.schema.response.balance.type == "string":
        # Parse string, convert to pence
        pounds = float(raw_response.accountBalance.amount)
        return int(pounds * 100)

# Store normalized data in graph
graph.create_node("Account", {
    "id": account_id,
    "balance_pence": normalize_balance(bank, response),  # Always pence
    "currency": "GBP",
    "source_bank": bank,
    "source_contract": contract.contract_id
})

Result: Graph contains semantically consistent data regardless of source bank.

Graph Schema Example

Nodes:
  Customer
    - id (UUID)
    - name
    - type (retail, corporate, private)

  Account
    - id (IBAN or internal ID)
    - balance_pence (integer, normalized)
    - currency (ISO 4217)
    - balance_type (available, ledger)
    - source_bank (internal, bank_a, bank_b)
    - source_contract (contract version)

  Portfolio
    - id
    - name
    - strategy (growth, income, balanced)

  ExternalAccount
    - id
    - bank_identifier
    - consent_status (active, revoked)
    - consent_expiry

Edges:
  Customer -[owns]-> Account
  Customer -[partOf]-> Household
  Account -[partOf]-> Portfolio
  Account -[linkedTo]-> ExternalAccount
  Account -[hasTransaction]-> Transaction
  Transaction -[beneficiary]-> Entity


메타데이터
post_id
de5f3d0579a4
slug
day-71-1-3-agentic-ai-in-open-banking-building-on-open-data-contracts-semantic-guarantees-de5f3d0579a4
url
https://medium.com/youme-technology/day-71-1-3-agentic-ai-in-open-banking-building-on-open-data-contracts-semantic-guarantees-de5f3d0579a4
canonical_url
https://medium.com/youme-technology/day-71-1-3-agentic-ai-in-open-banking-building-on-open-data-contracts-semantic-guarantees-de5f3d0579a4
author_url
https://medium.com/@luxananda
status
ok
fetched_at
2026-06-13 16:00:06