# Digital Twin — Developer Portal (full text) # Digital Twin — Digital Twin Docs Overview ## Digital Twin in the integration landscape Digital Twin is not a customer-facing product. It exposes its capabilities to digital channels, card processors, payment processors and other systems — never to the end user directly. Those systems send transactions; Digital Twin adjusts one or more balances across one or more accounts and returns an authoritative, real-time result. 12k+ TPS across 145M accounts 3k+ TPS in a single account 24×7 Available even when the Core is down Why Digital Twin ## Two value propositions — that combine Digital Twin delivers two things to the bank. It can do either one alone, or both together — and that's why it's worth reading on. Proposition 1 Batch DDA → real-time for the user Shadows the mainframe DDA and makes it available 24×7 from the user's point of view — even when the mainframe's batch processing is down. Proposition 2 Stablecoins & crypto side-by-side with fiat Adds stablecoins and other cryptos as new 24×7 assets, with the same banking experience — without needing to shadow the mainframe. Combined: USD and USDC sit 24×7 side by side, shadowing USD into Digital Twin. Or the bank starts with crypto-only 24×7 — great for POCs — and adds fiat later. Explore the docs Architecture & cloud → How Digital Twin is deployed and who talks to it. Ledger model → Multi-balance accounts and transaction codes. REST APIs → Synchronous endpoints for posts and reads. AVRO / Kafka messages → Asynchronous topic-based integrations. Matera · Digital Twin — integration documentation --- # Digital Twin — Architecture Architecture # Digital Twin as the foundation Digital Twin is the foundation that connects two worlds. Above it, the systems that cannot stop — digital channels, card processors, FedNow, RTP and blockchain — run 24×7. Below it, systems of record and reporting (Mainframe DDA, GL and others) can be unavailable for a few hours without affecting any real-time obligation of the bank. M 24×7 layer — cannot stop Digital channels mobile & internet banking Card processors authorize / post Payment processors FedNow · RTP Blockchain Fireblocks · Circle ↕ DIGITAL TWIN The foundation connecting both worlds · always 24×7 24×7 ↕ Core-banking adaptor playbook, not a product — bank-built software · 2-way async replication (CDC ↔ Kafka) CDC: mainframe → DTw Kafka: DTw → mainframe ↕ Non-24×7 layer — may be down for hours Mainframe (DDA) system of record · shadowed General Ledger (GL) accounting Regulatory & reporting end-of-day / month Figure 1 — Digital Twin is the 24×7 foundation. Above, systems that cannot stop connect to it; below, systems of record and non-real-time reporting can go down for hours. Digital Twin bridges both worlds. The core-banking adaptor is a playbook, not a product It is custom software the bank builds following our playbook. It performs asynchronous two-way replication between the mainframe and Digital Twin: (1) it replicates every mainframe transaction into Digital Twin via Change Data Capture (CDC); and (2) it consumes any new Digital Twin transaction from Kafka and pushes it back to the mainframe. This bidirectional, asynchronous replication is what implements eventual consistency between the two systems. ## Cloud architecture After the big picture above — where Digital Twin sits alongside the other business microservices — this is the technical view: Digital Twin alone and the technology it depends on to run in the cloud. It is cloud-agnostic and scales horizontally. VPC — network isolation API Gateway + mTLS ingress · OpenID Connect ↓ Kubernetes cluster Ledger service stateless pods · autoscaling actuator /health Transaction service applies transaction codes actuator /health Sync service shadows the mainframe actuator /health ↓ Aurora RDS relational store · Multi-AZ Kafka + Avro Registry events · versioned schemas Immutable journal append-only · audit Health monitoring scrapes the actuator APIs ← /actuator/health Figure 2 — Digital Twin's cloud view: services on Kubernetes inside a VPC, with Aurora RDS, Kafka + Avro Registry, and actuator APIs exposed to health-monitoring systems. ← Overview Ledger model → --- # Digital Twin — Ledger Model Ledger model # Multi-asset and multi-balance Digital Twin is multi-asset: each asset or currency lives in its own account — different assets are never mixed in one account. And it is multi-balance: within an account, the same asset can be split across several balances. Each transaction code determines which balance is affected. Practically any position a customer holds at the bank can be tracked by Digital Twin: fiat currencies (USD, CAD, BRL), crypto (USDC, USDT, BTC, ETH), loyalty points, mutual-fund shares, quantity of stocks, bonds, CDs, and so on. Multi-asset — one account per asset ACC-USD-001 USD fiat currency ACC-USDC-001 USDC stablecoin ACC-FUND-001 Fund shares quantity of shares Multi-balance — balances within an account Account ACC-USDC-001 · asset USDC The same asset, split into balances by state. Available1,250.00 Frozen (OFAC)500.00 Asset total1,750.00 Account ACC-USD-001 · asset USD Deposited check awaiting clearing. Available4,820.10 Held / blocked300.00 Asset total5,120.10 Sequence — check deposit of 5,000 (account ACC-USD-001) Three transaction codes over time. The asset total never changes — the amount only migrates from Blocked to Available. TC-301 · Check deposit Blocked+ 5,000.00 Amount enters as blocked, awaiting check-clearing confirmation. TC-302 · Courtesy release (10%) Blocked− 500.00 Available+ 500.00 10% of 5,000 released immediately as a courtesy to the client. TC-303 · Release after 48h Blocked− 4,500.00 Available+ 4,500.00 After 48 hours the remaining 4,500 is released. Blocked = 0; Available = 5,000. A just-received credit from a sanctioned blockchain address is moved into the frozen balance — same asset, same account, different balance. Available− 500.00 Frozen+ 500.00 The asset total is unchanged — the amount only moves between balances. The transaction code defines which balances are affected and in which direction. Principle: balances are never computed at read time. Each transaction is applied and recorded immutably, so a balance is always the deterministic sum of the journal. ← Architecture Security & access → --- # Digital Twin — Security Security & access # APIs for systems, not end users Digital Twin is never exposed to the end user. API consumers are digital channels, card and payment processors, and equivalent partners. Each consumer is an authenticated, authorized system acting on behalf of end customers. mTLS + OAuth2 Each partner uses mutual certificates and client credentials scoped per domain. Per-domain scopes The ACH processor only posts ACH transactions; channels only read authorized balances. Idempotency Every post carries an idempotency key to prevent double-spending. Immutable audit Every access and transaction is recorded immutably for audit. Trust model: partners are first-class systems. There is no end-user session in Digital Twin — the end customer's identity travels as a transaction attribute, authorized by the partner. ## OpenID Connect — per-microservice authorization Digital Twin's APIs are never exposed to the end user — only to other microservices, which in turn may be reached by third parties through the bank's own authentication, identification and access-control system. To secure those calls, Digital Twin uses OpenID Connect (for example, Keycloak) to authorize every API call. This lets the bank control — per microservice — exactly which APIs each client may call. Each microservice authenticates against the OpenID provider and receives a token carrying its granted scopes; Digital Twin validates the token and enforces those restrictions on every request. OpenID provider (Keycloak) Issues tokens to each client microservice; the bank manages realms, clients and roles. Per-API scopes A microservice can only call the APIs granted in its token — restrictions set by the bank. Validation on every call Digital Twin validates the token signature, issuer and scopes before processing. ``` Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6... // validated claims (issued by Keycloak) { "iss": "https://id.bank.example/realms/digital-twin", "azp": "ach-processor", // calling microservice "scope": "tx:post:ach balances:read", // APIs this service may call "exp": 1782226800 } ``` The call above may only post ACH transactions and read balances. Calling any API outside its scope returns 403. ← Ledger model REST APIs → --- # Digital Twin — REST APIs REST APIs # Digital Twin REST endpoints Synchronous endpoints, secured by OAuth2 (mTLS at the gateway). Post a transaction, and read balances/statements. The same operations also exist asynchronously over Kafka — see AVRO / Kafka messages. ← Security & access AVRO / Kafka messages → > All REST is OAuth2-secured (Bearer token). An account is identified by branch + account; an account group by kind + membershipId (membershipId is the group id). The Transaction operations also exist asynchronously over Kafka — see AVRO / Kafka messages. ## POST /v1/accounts/{branch}/{account}/batches Approves or declines a group of transactions (credits, debits, blocks, releases, reversals) against an account in a single atomic batch. Requires the transact role. **Query parameters** - `balances` — Balances to return, comma-separated; "all" or "none". - `X-Generate-Balance-Snapshot` — Header; if true, forces a balance snapshot. **Request body · EntryBatchRequest** ``` { "entries": [ { "entryKind": "ledger", "correlationId": "770109dc-7564-4558-bcf4-ffe7e46c5a38", "status": "POSTED", "description": "ATM withdrawal", "historyCode": 4919, "settlementInfo": { "fulfillment": "TOTAL", "amount": { "value": 5000, "currency": "USD" } } } ] } ``` **201 Response · EntryBatchGroupResponse** ``` { "createdAt": "2026-06-27T17:32:28Z", "entries": [ { "id": "770109dc-7564-4558-bcf4-ffe7e46c5a38", "entryKind": "ledger", "historyCode": 4919, "status": "POSTED", "amount": { "value": 5000, "currency": "USD" }, "balanceChanges": { "AVAILABLE": { "value": 5000, "currency": "USD" } } } ], "balances": { "AVAILABLE": { "amount": { "value": 15000, "currency": "USD" } } } } ``` **Errors** - `400` — Invalid attribute in the request (e.g. account not found, inactive account). - `402` — Insufficient funds. - `410` — The target resource is gone and cannot be used. - `422` — Request understood but could not be processed (e.g. already completed). - `503` — Service temporarily unavailable (e.g. account locked or too many concurrent requests). - `500` — Unexpected server error. ## DELETE /v1/accounts/{branch}/{account}/entries?correlationId=… Deletes one or more transactions from an account, identified by their correlation IDs. Requires the transact role. **Query parameters** - `correlationId` — Required. One or more correlation IDs of the transactions to delete (1-100). Returns 204 No Content on success; there is no response body. **Errors** - `400` — Invalid attribute in the request (e.g. account not found, inactive account). - `402` — Insufficient funds. - `503` — Service temporarily unavailable (e.g. account locked or too many concurrent requests). - `500` — Unexpected server error. ## GET /v1/accounts/{branch}/{account}/monitorings Lists the monitoring services set up for an account. Requires the view-monitorings role. **Query parameters** - `page` — Page index from 0 (default 0). - `size` — Page size, 1-100 (default 20). - `correlationIds` — Optional filter: monitoring correlation IDs (1-100). **200 Response · MonitoringsAccountResponse** ``` { "monitorings": [ { "id": "00000000-0000-0000-0000-000000000000", "status": "ACTIVE", "amount": { "value": 5000, "currency": "USD" }, "amountDue": { "value": 5000, "currency": "USD" } } ] } ``` **Errors** - `404` — The requested resource was not found. - `401` — Authentication failed or token missing. - `403` — User lacks the required role for this operation. - `500` — Unexpected server error. ## POST /v1/accounts/{branch}/{account}/monitorings Sets up a monitoring service on an account that fires a blueprint transaction when a matching credit arrives. Requires the modify-monitorings role. **Request body · MonitoringRequest** ``` { "monitoringKind": "ledger", "correlationId": "770109dc-7564-4558-bcf4-ffe7e46c5a38", "amount": { "value": 5000, "currency": "USD" }, "status": "ACTIVE", "blueprint": { "entryKind": "ledger", "historyCode": 4919, "description": "Scheduled credit" } } ``` **201 Response · MonitoringResponse** ``` { "id": "00000000-0000-0000-0000-000000000000", "status": "ACTIVE", "amount": { "value": 5000, "currency": "USD" }, "amountDue": { "value": 5000, "currency": "USD" }, "blueprint": { "id": "770109dc-7564-4558-bcf4-ffe7e46c5a38", "entryKind": "ledger", "historyCode": 4919 } } ``` **Errors** - `400` — Invalid attribute in the request (e.g. account not found, inactive account). - `402` — Insufficient funds. - `410` — The target resource is gone and cannot be used. - `503` — Service temporarily unavailable (e.g. account locked or too many concurrent requests). - `500` — Unexpected server error. ## PATCH /v1/accounts/{branch}/{account}/monitorings?correlationId=… Updates a monitoring service identified by its correlation ID. Requires the modify-monitorings role and a matching if-match version header. **Query parameters** - `if-match` — Required header. Current monitoring version. - `correlationId` — Required. Single correlation ID of the monitoring to update. **Request body · MonitoringPatchRequest** ``` { "status": "INACTIVE", "amount": { "value": 3000, "currency": "USD" } } ``` **200 Response · MonitoringResponse** ``` { "id": "00000000-0000-0000-0000-000000000000", "status": "ACTIVE", "amount": { "value": 5000, "currency": "USD" }, "amountDue": { "value": 5000, "currency": "USD" }, "blueprint": { "id": "770109dc-7564-4558-bcf4-ffe7e46c5a38", "entryKind": "ledger", "historyCode": 4919 } } ``` **Errors** - `400` — Invalid attribute in the request (e.g. account not found, inactive account). - `402` — Insufficient funds. - `410` — The target resource is gone and cannot be used. - `412` — Precondition failed: the if-match version does not match the current monitoring version. - `503` — Service temporarily unavailable (e.g. account locked or too many concurrent requests). - `500` — Unexpected server error. ## GET /v1/accounts/{branch}/{account}/monitorings/{id}/entries Lists the transactions generated by a monitoring service for an account. Requires the view-monitorings role. **Query parameters** - `page` — Page index from 0 (default 0). - `size` — Page size, 1-100 (default 20). **200 Response · MonitoringAccountEntriesResponse** ``` { "monitoringEntries": [ { "id": "770109dc-7564-4558-bcf4-ffe7e46c5a38", "entryKind": "ledger", "status": "POSTED", "branch": 4919, "account": 223166, "amount": { "value": 5000, "currency": "USD" }, "createdAt": "2026-06-27T17:32:28Z" } ] } ``` **Errors** - `401` — Authentication failed or token missing. - `403` — User lacks the required role for this operation. - `500` — Unexpected server error. ## GET /v1/accounts/{branch}/{account}/monitorings/{id} Retrieves the details of a single monitoring service for an account by its ID. Requires the view-monitorings role. **200 Response · MonitoringResponse** ``` { "id": "00000000-0000-0000-0000-000000000000", "status": "ACTIVE", "amount": { "value": 5000, "currency": "USD" }, "amountDue": { "value": 5000, "currency": "USD" }, "blueprint": { "id": "770109dc-7564-4558-bcf4-ffe7e46c5a38", "entryKind": "ledger", "historyCode": 4919 } } ``` **Errors** - `404` — The requested resource was not found. - `401` — Authentication failed or token missing. - `403` — User lacks the required role for this operation. - `500` — Unexpected server error. ## PATCH /v1/accounts/{branch}/{account}/monitorings/{id} Updates a monitoring service for an account by its ID. Requires the modify-monitorings role and a matching if-match version header. **Query parameters** - `if-match` — Required header. Current monitoring version. **Request body · MonitoringPatchRequest** ``` { "status": "INACTIVE", "amount": { "value": 3000, "currency": "USD" } } ``` **200 Response · MonitoringResponse** ``` { "id": "00000000-0000-0000-0000-000000000000", "status": "ACTIVE", "amount": { "value": 5000, "currency": "USD" }, "amountDue": { "value": 5000, "currency": "USD" }, "blueprint": { "id": "770109dc-7564-4558-bcf4-ffe7e46c5a38", "entryKind": "ledger", "historyCode": 4919 } } ``` **Errors** - `400` — Invalid attribute in the request (e.g. account not found, inactive account). - `402` — Insufficient funds. - `410` — The target resource is gone and cannot be used. - `412` — Precondition failed: the if-match version does not match the current monitoring version. - `503` — Service temporarily unavailable (e.g. account locked or too many concurrent requests). - `500` — Unexpected server error. ## POST /v1/accounts/{branch}/{account}/monitorings/cancel?correlationId=… Cancels a monitoring service for an account, identified by either its ID or correlation ID. Requires the modify-monitorings role. **Query parameters** - `id` — Optional. Single monitoring ID to cancel. - `correlationId` — Optional. Single correlation ID to cancel. **200 Response · MonitoringResponse** ``` { "id": "00000000-0000-0000-0000-000000000000", "status": "ACTIVE", "amount": { "value": 5000, "currency": "USD" }, "amountDue": { "value": 5000, "currency": "USD" }, "blueprint": { "id": "770109dc-7564-4558-bcf4-ffe7e46c5a38", "entryKind": "ledger", "historyCode": 4919 } } ``` **Errors** - `400` — Invalid attribute in the request (e.g. account not found, inactive account). - `402` — Insufficient funds. - `410` — The target resource is gone and cannot be used. - `412` — Precondition failed: the if-match version does not match the current monitoring version. - `422` — The monitoring service cannot be cancelled in its current state. - `503` — Service temporarily unavailable (e.g. account locked or too many concurrent requests). - `500` — Unexpected server error. ## POST /v1/accounts/{branch}/{account}/monitorings/{id}/release Deprecated. Releases a monitored sum into an account, optionally debiting at the same time via blueprints. Requires the release-monitorings role and an if-match header. Deprecated — use POST /v2/accounts/{branch}/{account}/monitorings/{id}/release. **Query parameters** - `if-match` — Required header. Current monitoring version. **Request body · MonitoringReleaseRequestV1** ``` { "settlementInfo": { "fulfillment": "TOTAL", "amount": { "value": 5000, "currency": "USD" } }, "blueprints": [] } ``` **200 Response · MonitoringReleaseResponse** ``` { "id": "00000000-0000-0000-0000-000000000000", "status": "REALIZED", "releasedAmount": { "value": 5000, "currency": "USD" } } ``` **Errors** - `400` — Invalid attribute in the request (e.g. account not found, inactive account). - `402` — Insufficient funds. - `410` — The target resource is gone and cannot be used. - `412` — Precondition failed: the if-match version does not match the current monitoring version. - `503` — Service temporarily unavailable (e.g. account locked or too many concurrent requests). - `500` — Unexpected server error. ## POST /v2/accounts/{branch}/{account}/monitorings/{id}/release Releases a monitored sum into an account by monitoring ID, optionally debiting at the same time via blueprints. Requires the release-monitorings role. **Request body · MonitoringReleaseRequest** ``` { "correlationId": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d", "settlementInfo": { "fulfillment": "TOTAL", "amount": { "value": 5000, "currency": "USD" } }, "blueprints": [ { "entryKind": "ledger", "correlationId": "f71b6072-3ce5-408a-91db-77a5ba7912ae", "historyCode": 4919, "settlementInfo": { "fulfillment": "TOTAL", "amount": { "value": 5000, "currency": "USD" } } } ] } ``` **200 Response · MonitoringReleaseResponse** ``` { "id": "00000000-0000-0000-0000-000000000000", "status": "REALIZED", "releasedAmount": { "value": 5000, "currency": "USD" } } ``` **Errors** - `400` — Invalid attribute in the request (e.g. account not found, inactive account). - `402` — Insufficient funds. - `409` — Conflicting correlation for the release request. - `410` — The target resource is gone and cannot be used. - `412` — Precondition failed: the if-match version does not match the current monitoring version. - `503` — Service temporarily unavailable (e.g. account locked or too many concurrent requests). - `500` — Unexpected server error. ## POST /v2/accounts/{branch}/{account}/monitorings/release?correlationId=… Releases a monitored sum into an account, addressing the monitoring by its correlation ID. Requires the release-monitorings role. **Query parameters** - `correlationId` — Required. Single correlation ID of the monitoring to release. **Request body · MonitoringReleaseRequest** ``` { "correlationId": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d", "settlementInfo": { "fulfillment": "TOTAL", "amount": { "value": 5000, "currency": "USD" } }, "blueprints": [ { "entryKind": "ledger", "correlationId": "f71b6072-3ce5-408a-91db-77a5ba7912ae", "historyCode": 4919, "settlementInfo": { "fulfillment": "TOTAL", "amount": { "value": 5000, "currency": "USD" } } } ] } ``` **200 Response · MonitoringReleaseResponse** ``` { "id": "00000000-0000-0000-0000-000000000000", "status": "REALIZED", "releasedAmount": { "value": 5000, "currency": "USD" } } ``` **Errors** - `400` — Invalid attribute in the request (e.g. account not found, inactive account). - `402` — Insufficient funds. - `409` — Conflicting correlation for the release request. - `410` — The target resource is gone and cannot be used. - `412` — Precondition failed: the if-match version does not match the current monitoring version. - `503` — Service temporarily unavailable (e.g. account locked or too many concurrent requests). - `500` — Unexpected server error. ## GET /v1/account-groups/{kind}/{membershipId}/monitorings Lists the monitoring services set up for a group of accounts (membershipId is the group ID, kind is the group type). Requires the view-monitorings role. **Query parameters** - `page` — Page index from 0 (default 0). - `size` — Page size, 1-100 (default 20). - `correlationIds` — Optional filter: monitoring correlation IDs (1-100). **200 Response · MonitoringsAccountResponse** ``` { "monitorings": [ { "id": "00000000-0000-0000-0000-000000000000", "status": "ACTIVE", "amount": { "value": 5000, "currency": "USD" }, "amountDue": { "value": 5000, "currency": "USD" } } ] } ``` **Errors** - `404` — The requested resource was not found. - `401` — Authentication failed or token missing. - `403` — User lacks the required role for this operation. - `500` — Unexpected server error. ## POST /v1/account-groups/{kind}/{membershipId}/monitorings Sets up a monitoring service across a group of accounts. Requires the modify-monitorings role. **Request body · MonitoringRequest** ``` { "monitoringKind": "ledger", "correlationId": "770109dc-7564-4558-bcf4-ffe7e46c5a38", "amount": { "value": 5000, "currency": "USD" }, "status": "ACTIVE", "blueprint": { "entryKind": "ledger", "historyCode": 4919, "description": "Scheduled credit" } } ``` **201 Response · MonitoringResponse** ``` { "id": "00000000-0000-0000-0000-000000000000", "status": "ACTIVE", "amount": { "value": 5000, "currency": "USD" }, "amountDue": { "value": 5000, "currency": "USD" }, "blueprint": { "id": "770109dc-7564-4558-bcf4-ffe7e46c5a38", "entryKind": "ledger", "historyCode": 4919 } } ``` **Errors** - `400` — Invalid attribute in the request (e.g. account not found, inactive account). - `402` — Insufficient funds. - `410` — The target resource is gone and cannot be used. - `503` — Service temporarily unavailable (e.g. account locked or too many concurrent requests). - `500` — Unexpected server error. ## PATCH /v1/account-groups/{kind}/{membershipId}/monitorings?correlationId=… Updates a group monitoring service identified by its correlation ID. Requires the modify-monitorings role and a matching if-match version header. **Query parameters** - `if-match` — Required header. Current monitoring version. - `correlationId` — Required. Single correlation ID of the monitoring to update. **Request body · MonitoringPatchRequest** ``` { "status": "INACTIVE", "amount": { "value": 3000, "currency": "USD" } } ``` **200 Response · MonitoringResponse** ``` { "id": "00000000-0000-0000-0000-000000000000", "status": "ACTIVE", "amount": { "value": 5000, "currency": "USD" }, "amountDue": { "value": 5000, "currency": "USD" }, "blueprint": { "id": "770109dc-7564-4558-bcf4-ffe7e46c5a38", "entryKind": "ledger", "historyCode": 4919 } } ``` **Errors** - `400` — Invalid attribute in the request (e.g. account not found, inactive account). - `402` — Insufficient funds. - `410` — The target resource is gone and cannot be used. - `412` — Precondition failed: the if-match version does not match the current monitoring version. - `503` — Service temporarily unavailable (e.g. account locked or too many concurrent requests). - `500` — Unexpected server error. ## GET /v1/account-groups/{kind}/{membershipId}/monitorings/{id}/entries Lists the transactions generated by a monitoring service across a group of accounts. Requires the view-monitorings role. **Query parameters** - `page` — Page index from 0 (default 0). - `size` — Page size, 1-100 (default 20). **200 Response · MonitoringAccountEntriesResponse** ``` { "monitoringEntries": [ { "id": "770109dc-7564-4558-bcf4-ffe7e46c5a38", "entryKind": "ledger", "status": "POSTED", "branch": 4919, "account": 223166, "amount": { "value": 5000, "currency": "USD" }, "createdAt": "2026-06-27T17:32:28Z" } ] } ``` **Errors** - `404` — The requested resource was not found. - `401` — Authentication failed or token missing. - `403` — User lacks the required role for this operation. - `500` — Unexpected server error. ## GET /v1/account-groups/{kind}/{membershipId}/monitorings/{id} Retrieves the details of a single group monitoring service by its ID. Requires the view-monitorings role. **200 Response · MonitoringResponse** ``` { "id": "00000000-0000-0000-0000-000000000000", "status": "ACTIVE", "amount": { "value": 5000, "currency": "USD" }, "amountDue": { "value": 5000, "currency": "USD" }, "blueprint": { "id": "770109dc-7564-4558-bcf4-ffe7e46c5a38", "entryKind": "ledger", "historyCode": 4919 } } ``` **Errors** - `404` — The requested resource was not found. - `401` — Authentication failed or token missing. - `403` — User lacks the required role for this operation. - `500` — Unexpected server error. ## PATCH /v1/account-groups/{kind}/{membershipId}/monitorings/{id} Updates a group monitoring service by its ID. Requires the modify-monitorings role and a matching if-match version header. **Query parameters** - `if-match` — Required header. Current monitoring version. **Request body · MonitoringPatchRequest** ``` { "status": "INACTIVE", "amount": { "value": 3000, "currency": "USD" } } ``` **200 Response · MonitoringResponse** ``` { "id": "00000000-0000-0000-0000-000000000000", "status": "ACTIVE", "amount": { "value": 5000, "currency": "USD" }, "amountDue": { "value": 5000, "currency": "USD" }, "blueprint": { "id": "770109dc-7564-4558-bcf4-ffe7e46c5a38", "entryKind": "ledger", "historyCode": 4919 } } ``` **Errors** - `400` — Invalid attribute in the request (e.g. account not found, inactive account). - `402` — Insufficient funds. - `410` — The target resource is gone and cannot be used. - `412` — Precondition failed: the if-match version does not match the current monitoring version. - `503` — Service temporarily unavailable (e.g. account locked or too many concurrent requests). - `500` — Unexpected server error. ## POST /v1/account-groups/{kind}/{membershipId}/monitorings/cancel?correlationId=… Cancels a group monitoring service, identified by either its ID or correlation ID. Requires the modify-monitorings role. **Query parameters** - `id` — Optional. Single monitoring ID to cancel. - `correlationId` — Optional. Single correlation ID to cancel. **200 Response · MonitoringResponse** ``` { "id": "00000000-0000-0000-0000-000000000000", "status": "ACTIVE", "amount": { "value": 5000, "currency": "USD" }, "amountDue": { "value": 5000, "currency": "USD" }, "blueprint": { "id": "770109dc-7564-4558-bcf4-ffe7e46c5a38", "entryKind": "ledger", "historyCode": 4919 } } ``` **Errors** - `400` — Invalid attribute in the request (e.g. account not found, inactive account). - `402` — Insufficient funds. - `410` — The target resource is gone and cannot be used. - `412` — Precondition failed: the if-match version does not match the current monitoring version. - `422` — The monitoring service cannot be cancelled in its current state. - `503` — Service temporarily unavailable (e.g. account locked or too many concurrent requests). - `500` — Unexpected server error. ## POST /v1/account-groups/{kind}/{membershipId}/monitorings/{id}/release Deprecated. Releases a monitored sum into a group of accounts, optionally debiting via blueprints. Requires the release-monitorings role and an if-match header. Deprecated — use POST /v2/account-groups/{kind}/{membershipId}/monitorings/{id}/release. **Query parameters** - `if-match` — Required header. Current monitoring version. **Request body · MonitoringReleaseRequestV1** ``` { "settlementInfo": { "fulfillment": "TOTAL", "amount": { "value": 5000, "currency": "USD" } }, "blueprints": [] } ``` **200 Response · MonitoringReleaseResponse** ``` { "id": "00000000-0000-0000-0000-000000000000", "status": "REALIZED", "releasedAmount": { "value": 5000, "currency": "USD" } } ``` **Errors** - `400` — Invalid attribute in the request (e.g. account not found, inactive account). - `402` — Insufficient funds. - `410` — The target resource is gone and cannot be used. - `412` — Precondition failed: the if-match version does not match the current monitoring version. - `503` — Service temporarily unavailable (e.g. account locked or too many concurrent requests). - `500` — Unexpected server error. ## POST /v2/account-groups/{kind}/{membershipId}/monitorings/{id}/release Releases a monitored sum into a group of accounts by monitoring ID, optionally debiting via blueprints. Requires the release-monitorings role. **Request body · MonitoringReleaseRequest** ``` { "correlationId": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d", "settlementInfo": { "fulfillment": "TOTAL", "amount": { "value": 5000, "currency": "USD" } }, "blueprints": [ { "entryKind": "ledger", "correlationId": "f71b6072-3ce5-408a-91db-77a5ba7912ae", "historyCode": 4919, "settlementInfo": { "fulfillment": "TOTAL", "amount": { "value": 5000, "currency": "USD" } } } ] } ``` **200 Response · MonitoringReleaseResponse** ``` { "id": "00000000-0000-0000-0000-000000000000", "status": "REALIZED", "releasedAmount": { "value": 5000, "currency": "USD" } } ``` **Errors** - `400` — Invalid attribute in the request (e.g. account not found, inactive account). - `402` — Insufficient funds. - `409` — Conflicting correlation for the release request. - `410` — The target resource is gone and cannot be used. - `412` — Precondition failed: the if-match version does not match the current monitoring version. - `503` — Service temporarily unavailable (e.g. account locked or too many concurrent requests). - `500` — Unexpected server error. ## POST /v2/account-groups/{kind}/{membershipId}/monitorings/release?correlationId=… Releases a monitored sum into a group of accounts, addressing the monitoring by its correlation ID. Requires the release-monitorings role. **Query parameters** - `correlationId` — Required. Single correlation ID of the monitoring to release. **Request body · MonitoringReleaseRequest** ``` { "correlationId": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d", "settlementInfo": { "fulfillment": "TOTAL", "amount": { "value": 5000, "currency": "USD" } }, "blueprints": [ { "entryKind": "ledger", "correlationId": "f71b6072-3ce5-408a-91db-77a5ba7912ae", "historyCode": 4919, "settlementInfo": { "fulfillment": "TOTAL", "amount": { "value": 5000, "currency": "USD" } } } ] } ``` **200 Response · MonitoringReleaseResponse** ``` { "id": "00000000-0000-0000-0000-000000000000", "status": "REALIZED", "releasedAmount": { "value": 5000, "currency": "USD" } } ``` **Errors** - `400` — Invalid attribute in the request (e.g. account not found, inactive account). - `402` — Insufficient funds. - `409` — Conflicting correlation for the release request. - `410` — The target resource is gone and cannot be used. - `412` — Precondition failed: the if-match version does not match the current monitoring version. - `503` — Service temporarily unavailable (e.g. account locked or too many concurrent requests). - `500` — Unexpected server error. ## GET /api/v2/contas/{agencia}/{conta}/saldos Re-exposes the legacy checking-account (Conta Corrente) balance contract. Returns all balance types recorded for the account on the reference date (D) or the previous business day (D-1). The DISPONIVEL (available) balance is always returned. **Query parameters** - `Authorization` — Required header. Bearer OAuth2 token. - `incluirLimite` — Whether the available balance includes the account credit limit (default true). - `dMenosUm` — If true, returns D-1 balances instead of the reference date D (default false). **200 Response · SaldoResponse** ``` { "data": "2026-06-27", "saldoDisponivel": 100.75, "saldoVinculado": 200.75, "saldoBloqueado": 50.0, "saldoContabil": 100.33, "saldoInvestimento": 100.0, "limiteCreditoDisponivel": 0 } ``` **Errors** - `400` — Client-side error (e.g. account not found). - `500` — Unexpected server error. ## GET /api/v1/contas/{agencia}/{conta}/saldo-periodo?dataInicial=… Re-exposes the legacy checking-account balance-by-period contract. Returns the account balances for a single date or across a date range. **Query parameters** - `Authorization` — Required header. Bearer OAuth2 token. - `dataInicial` — Required. Start date of the balance search (yyyy-MM-dd). - `dataFinal` — Optional end date; defaults to the system reference date. **200 Response · saldosPeriodoRetorno** ``` { "data": [ { "data": "2026-06-27", "saldoDisponivel": 100.75, "saldoVinculado": 200.75, "saldoBloqueado": 50.0, "saldoContabil": 100.33 } ] } ``` **Errors** - `400` — Client-side error (e.g. account not found). - `500` — Unexpected server error. ## GET /api/v1/accounts/{branch}/{account}/balance/{date} Re-exposes the legacy checking-account contract to check the available balance of an account on a given date. **Query parameters** - `Authorization` — Required header. Bearer OAuth2 token. - `date` — Path. Balance date (yyyy-MM-dd). **200 Response · availableBalanceByDateResponse** ``` { "availableBalance": 1500.0 } ``` **Errors** - `400` — Client-side error (e.g. account not found). - `500` — Unexpected server error. ## GET /api/v2/contas/{agencia}/{conta}/movimentoConsolidado?indFuturo=… Re-exposes the legacy checking-account contract to consolidate entries for statement building. Returns the net movement (total credits minus total debits) for the given filters. **Query parameters** - `Authorization` — Required header. Bearer OAuth2 token. - `indFuturo` — Required. [S] returns only future entries; [N] returns only reference-date entries. - `tipoLancamento` — Optional. [D] debits only, [C] credits only, or empty for both. With [D] the result is negative. **200 Response · movimentoConsolidadoRetorno** ``` { "totalMovimento": 100.01 } ``` **Errors** - `400` — Client-side error while processing the request. - `500` — Unexpected server error. ## GET /api/v1/contas/{agencia}/{conta}/extrato?dataInicial=… Re-exposes the legacy checking-account statement contract. Returns the entry statement for an account, by date range, from a start date, or for a number of recent days. Entries are ordered by reference date. **Query parameters** - `dataInicial` — Optional. Start date of entries to return (yyyy-MM-dd). - `dataFinal` — Optional. End date of entries to return (yyyy-MM-dd). - `qtdUltimosDias` — Optional. Returns entries from the last N calendar days. - `tipoData` — Optional. How date filters apply: LANCAMENTO (posting date), ENTRADA (entry date/time), or ENTRADA_LOTE. Default ENTRADA. **200 Response · ExtratoResponse** ``` { "data": { "agencia": 4919, "numConta": 223166, "lancamentos": [ { "dataLancamento": "2022-11-12", "dataEntrada": "2022-11-12T10:30:00", "tipoLancamento": "C", "valor": 739.39, "descHistorico": "PIX recebido", "idLancamento": 1001, "efetivado": true } ] } } ``` **Errors** - `400` — Client-side error (e.g. account not found). - `500` — Unexpected server error. ## GET /api/v1/entries?dateCategory=…&startDateTime=…&pageNumber=…&pageSize=… Re-exposes the legacy entry-listing contract. Returns a paginated list of entries filtered by date category and date range, optionally scoped by branch, account, and operation. **Query parameters** - `dateCategory` — Required. One of ENTRY_DATE, INCOMING_DATE, INPUT_BUNDLE_DATE, TRANSACTION_DATE. - `startDateTime` — Required. Start date-time (yyyy-MM-ddTHH:mm:ss). - `endDateTime` — Optional. End date-time; defaults to the start date. - `branch` — Optional. Bank branch filter. - `accountNumber` — Optional. Account number filter. - `operationCode` — Optional. Operation code filter. - `operationReference` — Optional. Operation reference filter. - `pageNumber` — Required. Current page (from 0). - `pageSize` — Required. Page size, 1-100. **200 Response · EntriesResponse** ``` { "data": { "entriesList": [ { "entryId": 123456789, "branch": 4919, "accountNumber": 223166, "entryDate": "2026-06-27", "entryType": "D", "amount": 50.0, "operationCode": 4919 } ], "pagination": { "pageNumber": 0, "totalPages": 1, "totalRecords": 1 } } } ``` **Errors** - `400` — Client-side error while processing the request. ## GET /v1/accounts Retrieves information for one or more accounts identified by their account keys. Accounts undergoing migration to Digital Twin are not returned. Requires the view-accounts role. **Query parameters** - `key` — Required. One or more account keys, each formed by branch and account number separated by a slash (e.g. 1234/12345); separate multiple keys with commas. - `page` — Zero-based page index to retrieve. Defaults to 0. - `size` — Maximum number of items per page (1-100). Defaults to 20. **200 Response · accountResponse** ``` { "accounts": [ { "branch": 4919, "account": 223166, "status": "ACTIVE", "timeZone": "America/Sao_Paulo", "type": { "id": "1", "description": "Checking account", "category": "checking" }, "features": [ { "name": "BLOCKED_FOR_DEBIT", "value": "true", "type": "BOOLEAN" } ], "memberships": [ { "id": "12345678901", "kind": "PERSON_HOLDER_TAX_ID" } ] } ] } ``` **Errors** - `400` — The request has an invalid attribute or parameter. - `401` — Authentication failed (missing or invalid token). - `500` — Unexpected server error. ## GET /v1/memberships/accounts Retrieves all accounts related to one or more membership keys (holder/group). Each membership key is a kind and id separated by a slash. Requires the view-account-memberships role. **Query parameters** - `membershipKey` — Required. One or more membership keys in the format [kind]/[id] (e.g. PERSON_HOLDER_TAX_ID/12345678901); separate multiple keys with commas. - `page` — Zero-based page index to retrieve. Defaults to 0. - `size` — Maximum number of items per page (1-100). Defaults to 20. **200 Response · accountResponse** ``` { "accounts": [ { "branch": 4919, "account": 223166, "status": "ACTIVE", "timeZone": "America/Sao_Paulo", "type": { "id": "1", "description": "Checking account", "category": "checking" }, "features": [ { "name": "BLOCKED_FOR_DEBIT", "value": "true", "type": "BOOLEAN" } ], "memberships": [ { "id": "12345678901", "kind": "PERSON_HOLDER_TAX_ID" } ] } ] } ``` **Errors** - `400` — The request has an invalid attribute or parameter. - `401` — Authentication failed (missing or invalid token). - `500` — Unexpected server error. ## GET /v1/accounts/{branch}/{account}/limits Lists all limits configured for an account. Requires the view-account-limits role. **Query parameters** - `branch` — Path parameter. The account branch; use 0 when the account has no branch number. - `account` — Path parameter. The account number or unique account identifier. **200 Response · limits** ``` { "limits": [ { "currency": "USD", "min": 100, "max": 100000, "startAt": "2023-07-01" }, { "currency": "USD", "min": 0, "max": 10000, "startAt": "2023-01-01", "dueAt": "2023-06-30" } ] } ``` **Errors** - `400` — The request has an invalid attribute or parameter. - `401` — Authentication failed (missing or invalid token). - `404` — The requested resource was not found. - `500` — Unexpected server error. ## GET /v1/accounts/{branch}/{account}/limits/{limitType} Retrieves a single account limit by its type. Requires the view-account-limits role. **Query parameters** - `branch` — Path parameter. The account branch; use 0 when the account has no branch number. - `account` — Path parameter. The account number or unique account identifier. - `limitType` — Path parameter. The limit type to return (e.g. OVERDRAFT). **200 Response · limitResult** ``` { "type": "OVERDRAFT", "limit": { "currency": "USD", "min": 1, "max": 10000 }, "lastUpdatedAt": "2019-01-03T00:00:00Z" } ``` **Errors** - `400` — The request has an invalid attribute or parameter. - `401` — Authentication failed (missing or invalid token). - `404` — The requested resource was not found. - `500` — Unexpected server error. ## PUT /v1/accounts/{branch}/{account}/limits/{limitType} Adds or updates an account limit of the given type. Requires the modify-account-limits role. **Query parameters** - `branch` — Path parameter. The account branch; use 0 when the account has no branch number. - `account` — Path parameter. The account number or unique account identifier. - `limitType` — Path parameter. The limit type to add or update. **Request body** ``` { "limit": { "currency": "USD", "min": 1, "max": 10000 } } ``` **200 Response · limitResult** ``` { "type": "OVERDRAFT", "limit": { "currency": "USD", "min": 1, "max": 10000 }, "lastUpdatedAt": "2019-01-03T00:00:00Z" } ``` **Errors** - `400` — The request has an invalid attribute or parameter. - `401` — Authentication failed (missing or invalid token). - `404` — The requested resource was not found. - `500` — Unexpected server error. ## DELETE /v1/accounts/{branch}/{account}/limits/{limitType} Deletes an account limit of the given type. Returns 204 No Content on success. Requires the modify-account-limits role. **Query parameters** - `branch` — Path parameter. The account branch; use 0 when the account has no branch number. - `account` — Path parameter. The account number or unique account identifier. - `limitType` — Path parameter. The limit type to delete. Returns 204 No Content on success — no response body. **Errors** - `400` — The request has an invalid attribute or parameter. - `401` — Authentication failed (missing or invalid token). - `404` — The requested resource was not found. - `500` — Unexpected server error. ## GET /v1/system/limits Lists all system-wide limits. Requires the view-system-limits role. **200 Response · limits** ``` [ { "type": "OVERDRAFT", "limit": { "currency": "USD", "min": 1, "max": 10000 }, "lastUpdatedAt": "2019-01-03T00:00:00Z" } ] ``` **Errors** - `400` — The request has an invalid attribute or parameter. - `401` — Authentication failed (missing or invalid token). - `500` — Unexpected server error. ## GET /v1/system/limits/{limitType} Retrieves a single system limit by its type. Requires the view-system-limits role. **Query parameters** - `limitType` — Path parameter. The system limit type to return (e.g. OVERDRAFT). **200 Response · limitResult** ``` { "type": "OVERDRAFT", "limit": { "currency": "USD", "min": 1, "max": 10000 }, "lastUpdatedAt": "2019-01-03T00:00:00Z" } ``` **Errors** - `400` — The request has an invalid attribute or parameter. - `401` — Authentication failed (missing or invalid token). - `404` — The requested resource was not found. - `500` — Unexpected server error. ## PUT /v1/system/limits/{limitType} Adds or updates a system limit of the given type. Requires the modify-system-limits role. **Query parameters** - `limitType` — Path parameter. The system limit type to add or update. **Request body** ``` { "limit": { "currency": "USD", "min": 1, "max": 10000 } } ``` **200 Response · limitResult** ``` { "type": "OVERDRAFT", "limit": { "currency": "USD", "min": 1, "max": 10000 }, "lastUpdatedAt": "2019-01-03T00:00:00Z" } ``` **Errors** - `400` — The request has an invalid attribute or parameter. - `401` — Authentication failed (missing or invalid token). - `500` — Unexpected server error. ## DELETE /v1/system/limits/{limitType} Deletes a system limit of the given type. Returns 204 No Content on success. Requires the modify-system-limits role. **Query parameters** - `limitType` — Path parameter. The system limit type to delete. Returns 204 No Content on success — no response body. **Errors** - `400` — The request has an invalid attribute or parameter. - `401` — Authentication failed (missing or invalid token). - `500` — Unexpected server error. ## GET /v1/balances Lists balances for a group of accounts. Returns the current balance when no date range is given, or balances grouped by date when a range is supplied. Requires the view-balances role. **Query parameters** - `accountKeys` — Required. One or more account keys formed by branch and account number separated by a slash (e.g. 1234/12345); separate multiple keys with commas. - `initialDate` — Start date of the range (YYYY-MM-DD). Omit both dates to get the current balance; if provided, finalDate is also required. - `finalDate` — End date of the range (YYYY-MM-DD). Required when initialDate is provided. - `balanceTypes` — Balance types to return, comma-separated (e.g. AVAILABLE,BLOCKED,HELD,BOOK). All types by default. - `currency` — Currency code (ISO 4217 or canonical crypto/stablecoin ticker). Defaults to the system currency. - `page` — Zero-based page index. Defaults to 0. - `size` — Maximum number of items per page (1-100). Defaults to 20. **200 Response · balancesBatchResponse** ``` { "balances": [ { "branch": 1, "account": 100, "balances": [ { "date": "2024-01-01", "balances": { "AVAILABLE": { "value": 1000, "currency": "USD" }, "HELD": { "value": 0, "currency": "USD" } } } ] } ] } ``` **Errors** - `400` — The request has an invalid attribute or parameter. - `404` — The requested resource was not found. - `500` — Unexpected server error. ## GET /v1/accounts/{branch}/{account}/balances Returns balances grouped by date for a single account over a date range. Requires the view-account-balances role. **Query parameters** - `branch` — Path parameter. The account branch. - `account` — Path parameter. The account number. - `initialDate` — Required. Start reference date of the range (YYYY-MM-DD). - `finalDate` — Required. End reference date of the range (YYYY-MM-DD). - `balanceTypes` — Required. Balance types to return, comma-separated (e.g. AVAILABLE,BLOCKED,HELD,BOOK). - `currency` — Required. Currency code (ISO 4217 or canonical crypto/stablecoin ticker). - `page` — Zero-based page index. Defaults to 0. - `size` — Maximum number of items per page (1-100). Defaults to 20. **200 Response · balancesResponse** ``` { "balances": [ { "date": "2022-07-06", "balances": { "AVAILABLE": { "value": 3000, "currency": "USD" }, "HELD": { "value": 2000, "currency": "USD" }, "BLOCKED": { "value": 500, "currency": "USD" }, "BOOK": { "value": 200, "currency": "USD" } } } ] } ``` **Errors** - `400` — The request has an invalid attribute or parameter. - `404` — The requested resource was not found. - `500` — Unexpected server error. ## GET /v1/accounts/{branch}/{account}/limits Lists the saved limits for an account, optionally filtered by a due-date range. Requires the view-account-limits role. **Query parameters** - `branch` — Path parameter. The account branch. - `account` — Path parameter. The account number. - `limitType` — Required. The limit type to search (e.g. OVERDRAFT). - `initialDueDate` — Start of the due-date range (YYYY-MM-DD). - `finalDueDate` — End of the due-date range (YYYY-MM-DD). - `page` — Zero-based page index. Defaults to 0. - `size` — Maximum number of items per page (1-100). Defaults to 20. **200 Response · limitsResponse** ``` { "limits": [ { "currency": "USD", "min": 100, "max": 100000, "startAt": "2023-07-01" }, { "currency": "USD", "min": 0, "max": 10000, "startAt": "2023-01-01", "dueAt": "2023-06-30" } ] } ``` **Errors** - `400` — The request has an invalid attribute or parameter. - `404` — The requested resource was not found. - `500` — Unexpected server error. ## GET /v1/cubes/balances Runs an OLAP query over balances using a Multidimensional Expression (MDX) and returns the resulting cube data. Requires the view-balance-cube role. **Query parameters** - `mdxQuery` — Required. The MDX query to evaluate against the balances cube. **200 Response · cubeResponse** ``` { "rows": 1, "result": [ { "AMOUNT": -200.0 } ] } ``` **Errors** - `400` — The request has an invalid attribute or parameter. - `500` — Unexpected server error. - `504` — Gateway timeout while processing the query. ## GET /v1/entries Lists transactions for an account to build a statement and consolidate values. Deprecated in favor of /v2/entries. Requires the view-entries role. **Query parameters** - `branch` — The account branch. Required if account is provided. - `account` — The account number. Required if branch is provided. - `historyCode` — Deprecated. A single history code categorizing the transaction; use historyCodes instead. - `historyCodes` — A list of history codes categorizing the transactions (max 1000). - `initialDate` — Required. Start date-time of the range (ISO 8601, e.g. 2017-07-21T17:32:28Z). - `finalDate` — Required. End date-time of the range (ISO 8601). - `dateType` — Required. ENTRY filters by the balance-impact reference date; EVENT filters by when the transaction was recorded. - `operationType` — Transaction type: C for credits or D for debits. Returns both if omitted; ignored when history codes are provided. - `currency` — Required. Currency code (ISO 4217 or canonical crypto/stablecoin ticker). - `fields` — ENTRIES returns only transactions; BALANCE_SUMMARY returns the synthetic sum. Both are returned if omitted. - `featureName` — Indexed metadata field name to filter by. Required if featureValue is provided. - `featureValue` — Value matching featureName in the transaction metadata. Required if featureName is provided. - `page` — Zero-based page index. Defaults to 0. - `size` — Maximum number of items per page (1-100). Defaults to 20. - `X-Count-Omit` — Header. When true, pagination headers (X-Total-Elements, X-Total-Pages, X-Has-Next) are omitted. **200 Response · entriesResponse** ``` { "entries": [ { "branch": 1, "account": 223166, "entryDate": "2022-03-24", "eventDate": "2017-07-21T17:32:28Z", "operationType": "C", "amount": { "value": 100000, "currency": "USD" }, "entryId": "1a3c5e7g9i", "entryKind": "LEDGER", "entryDescription": "Credit Card", "documentTypeKey": "9", "historyCode": 998, "historyDescription": "DOC" } ], "balanceSummary": { "value": 10000, "currency": "USD" } } ``` **Errors** - `400` — The request has an invalid attribute or parameter. - `404` — The requested resource was not found. - `500` — Unexpected server error. - `504` — Gateway timeout while processing the query. ## GET /v2/entries Lists transactions for an account with segregated posted and pending summaries. Requires the view-entries role. **Query parameters** - `branch` — The account branch. Required if account is provided. - `account` — The account number. Required if branch is provided. - `historyCodes` — A list of history codes categorizing the transactions (max 1000). - `initialDate` — Required. Start date-time of the range (ISO 8601, e.g. 2017-07-21T17:32:28Z). - `finalDate` — Required. End date-time of the range (ISO 8601). - `dateType` — Required. ENTRY filters by the balance-impact reference date; EVENT filters by when the transaction was recorded. - `operationType` — Transaction type: C for credits or D for debits. Returns both if omitted; ignored when history codes are provided. - `currency` — Required. Currency code (ISO 4217 or canonical crypto/stablecoin ticker). - `fields` — ENTRIES returns only transactions; BALANCE_SUMMARY returns the synthetic sum. Both are returned if omitted. - `featureName` — Indexed metadata field name to filter by. Required if featureValue is provided. - `featureValue` — Value matching featureName in the transaction metadata. Required if featureName is provided. - `type` — Filter entries by status: POSTED or PENDING. Both are returned if omitted. - `page` — Zero-based page index. Defaults to 0. - `size` — Maximum number of items per page (1-100). Defaults to 20. - `X-Count-Omit` — Header. When true, pagination headers (X-Total-Elements, X-Total-Pages, X-Has-Next) are omitted. **200 Response · entriesV2Response** ``` { "entries": [ { "branch": 1, "account": 230621, "entryDate": "2023-06-21", "eventDate": "2023-06-20T17:20:19Z", "operationType": "C", "amount": { "value": 20, "currency": "USD" }, "entryId": "1a3c5e7g9i", "entryKind": "LEDGER", "entryDescription": "Credit Card", "historyCode": 998, "type": "POSTED" } ], "balanceSummary": { "sumOfPostedEntries": { "value": 20, "currency": "USD" }, "sumOfPendingCreditEntries": { "value": 0, "currency": "USD" }, "sumOfPendingDebitEntries": { "value": 0, "currency": "USD" } } } ``` **Errors** - `400` — The request has an invalid attribute or parameter. - `404` — The requested resource was not found. - `500` — Unexpected server error. - `504` — Gateway timeout while processing the query. ## GET /v1/entries/metadata Returns the metadata attached to one or more transactions, identified by their entry IDs. Requires the view-entries-metadata role. **Query parameters** - `entriesId` — Required. One or more entry IDs, comma-separated. **200 Response · entriesMetadataResponse** ``` [ { "id": "1a3c5e7g9i", "metaData": { "key": "value", "key2": 1 } } ] ``` **Errors** - `400` — The request has an invalid attribute or parameter. - `404` — The requested resource was not found. - `500` — Unexpected server error. ## GET /v1/cubes/entries Runs an OLAP query over transactions using a Multidimensional Expression (MDX) and returns the resulting cube data. Requires the view-entry-cube role. **Query parameters** - `mdxQuery` — Required. The MDX query to evaluate against the entries cube. **200 Response · cubeResponse** ``` { "rows": 3, "result": [ { "[ACCOUNT CHECKING].[ACCOUNT]": "35478", "AMOUNT": "979.0", "[ACCOUNT CHECKING].[BRANCH]": "12" } ] } ``` **Errors** - `400` — The request has an invalid attribute or parameter. - `500` — Unexpected server error. - `504` — Gateway timeout while processing the query. ## POST /v1/transaction-bundles Posts credits and debits across multiple accounts as a single bundle operation (one atomic transaction). Requires the transact role on both the composite-transaction and transaction modules. Amounts are unscaled integers in minor units. **Request body** ``` { "bundle": "65461321315", "kind": "regular", "status": "POSTED", "entries": [ { "branch": 4919, "account": 223166, "entryKind": "ledger", "correlationId": "c6675439-25e1-4435-8449-5a17fa967d5c", "description": "ATM withdraw", "historyCode": 4919, "amount": { "value": 100000, "currency": "USD" } } ] } ``` **201 Response · entryBundleGroupResponse** ``` { "bundleId": "65461321315", "status": "POSTED", "bundleEntries": [ { "branch": 4919, "account": 223166, "createdAt": "2017-07-21T17:32:28Z", "entries": [ { "id": "c6675439-25e1-4435-8449-5a17fa967d5c", "entryKind": "ledger", "historyCode": 4919, "status": "POSTED", "amount": { "value": 100000, "currency": "USD" }, "balanceChanges": { "AVAILABLE": { "value": 100000, "currency": "USD" } } } ], "balances": { "AVAILABLE": { "amount": { "value": 100000, "currency": "USD" } } } } ] } ``` **Errors** - `400` — The request has an invalid attribute or parameter. - `401` — Authentication failed (missing or invalid token). - `402` — Insufficient funds. - `403` — The user lacks the required role for this operation. - `409` — Conflict while processing the bundle (e.g. idempotency hit or rollback in progress). - `410` — The entry or transaction is no longer accessible (previously removed). - `500` — Unexpected server error. Each bundle entry follows the same 'Required fields by entryKind' rules as POST .../batches. ## DELETE /v1/transaction-bundles/{bundle} Deletes (reverses) the transactions of a bundle operation. Returns 204 No Content on success. Requires the transact role on both the composite-transaction and transaction modules. **Query parameters** - `bundle` — Path parameter. The unique identifier of the bundle operation to delete. Returns 204 No Content on success — no response body. **Errors** - `400` — The request has an invalid attribute or parameter. - `401` — Authentication failed (missing or invalid token). - `402` — Insufficient funds. - `403` — The user lacks the required role for this operation. - `500` — Unexpected server error. ## POST /v1/accounts/{branch}/{account}/monitorings/{monitoringId}/release Releases a monitored (held) sum into an account, optionally debiting an amount in the same request via blueprints. Requires release/view/modify-monitorings roles on the transaction module and release-monitorings on composite-transaction. **Query parameters** - `branch` — Path parameter. The account branch; use 0 when the account has no branch number. - `account` — Path parameter. The account number or unique account identifier. - `monitoringId` — Path parameter. The unique identifier of the monitoring service. - `if-match` — Required header. The last registered version of the monitoring service. **Request body** ``` { "settlementInfo": { "fulfillment": "TOTAL", "amount": { "value": 100000, "currency": "USD" } }, "status": "INACTIVE", "blueprints": [ { "entryKind": "ledger", "correlationId": "c6675439-25e1-4435-8449-5a17fa967d5c", "historyCode": 4919, "settlementInfo": { "fulfillment": "TOTAL", "amount": { "value": 5000, "currency": "USD" } } } ] } ``` **200 Response · monitoringReleaseResponse** ``` { "monitoringId": "fd18bfce-5112-4221-ae54-818de9c2cdca", "correlations": { "dummy-originator": "130e2a5a-0c34-4810-b72f-dca85defe51e" }, "status": "REALIZED", "releasedAmount": { "value": 100000, "currency": "BRL" }, "totalAmount": { "value": 100000, "currency": "BRL" }, "amountDue": { "value": 1, "currency": "BRL" }, "totalReleased": { "value": 100000, "currency": "BRL" }, "entries": [ { "branch": 4919, "account": 223166, "createdAt": "2017-07-21T17:32:28Z", "id": "c6675439-25e1-4435-8449-5a17fa967d5c", "entryKind": "ledger", "status": "POSTED", "amount": { "value": 100000, "currency": "BRL" } } ] } ``` **Errors** - `400` — The request has an invalid attribute or parameter. - `401` — Authentication failed (missing or invalid token). - `403` — The user lacks the required role for this operation. - `500` — Unexpected server error. ## POST /v1/account-groups/{kind}/{membershipId}/monitorings/{monitoringId}/release Releases a monitored (held) sum into a group of accounts, optionally debiting an amount in the same request via blueprints. Requires release/view/modify-monitorings roles on the transaction module and release-monitorings on composite-transaction. **Query parameters** - `kind` — Path parameter. The group kind (e.g. HOLDER). - `membershipId` — Path parameter. The group identifier. - `monitoringId` — Path parameter. The unique identifier of the monitoring service. - `if-match` — Required header. The last registered version of the monitoring service. **Request body** ``` { "settlementInfo": { "fulfillment": "TOTAL", "amount": { "value": 100000, "currency": "USD" } }, "status": "INACTIVE", "blueprints": [ { "entryKind": "ledger", "correlationId": "c6675439-25e1-4435-8449-5a17fa967d5c", "historyCode": 4919, "settlementInfo": { "fulfillment": "TOTAL", "amount": { "value": 5000, "currency": "USD" } } } ] } ``` **200 Response · monitoringReleaseResponse** ``` { "monitoringId": "fd18bfce-5112-4221-ae54-818de9c2cdca", "correlations": { "dummy-originator": "130e2a5a-0c34-4810-b72f-dca85defe51e" }, "status": "REALIZED", "releasedAmount": { "value": 100000, "currency": "BRL" }, "totalAmount": { "value": 100000, "currency": "BRL" }, "amountDue": { "value": 1, "currency": "BRL" }, "totalReleased": { "value": 100000, "currency": "BRL" }, "entries": [ { "branch": 4919, "account": 223166, "createdAt": "2017-07-21T17:32:28Z", "id": "c6675439-25e1-4435-8449-5a17fa967d5c", "entryKind": "ledger", "status": "POSTED", "amount": { "value": 100000, "currency": "BRL" } } ] } ``` **Errors** - `400` — The request has an invalid attribute or parameter. - `401` — Authentication failed (missing or invalid token). - `403` — The user lacks the required role for this operation. - `500` — Unexpected server error. --- # Digital Twin — AVRO Kafka AVRO / Kafka # Asynchronous messages Not every Digital Twin "API" is REST. Many integrations are AVRO messages that partners publish to or consume from Kafka topics — the same operations as the REST page, asynchronously. ← REST APIs Mainframe shadowing → > Schemas are owned by dtw-events (Confluent schema registry). amount is an UNSCALED decimal in minor units ($14.85 → 1485). Force-post = a ledger entry with balanceUpdateStrategy = UNCONDITIONAL, allow-listed per history code — Kafka path only. is the per-environment topic prefix. ## PRODUCE `.dtw.transaction.operations.*.command` - **Key**: AccountKey (branch + account) - **Producers**: channels · processors · adapters - **Reply**: EntryCommandSuccessResult / …FailureResult **AVRO schema · EntryRequestCommand** ``` // namespace: com.matera.dtw.command.transaction.entry { "type": "record", "name": "EntryRequestCommand", "fields": [ { "name": "baseCommand", "type": "BaseCommand" }, // idempotencyId, traceId, correlationId, timestamp { "name": "account", "type": "AccountKey" }, // { branch: int, account: long } { "name": "entry", "type": [ // one of: "LedgerEntryRequestData", "BlockingEntryRequestData", "UnblockingEntryRequestData", "ReversalEntryRequestData", "RemovalEntryRequestData", "ExclusionEntryRequestData", "ReleaseEntryRequestData" ] } ]} // LedgerEntryRequestData — the common "post an entry" case { "type": "record", "name": "LedgerEntryRequestData", "fields": [ { "name": "historyCode", "type": "int" }, // the transaction code { "name": "amount", "type": "FinancialAmount" }, // { currency, amount } — amount UNSCALED { "name": "status", "type": "EntryStatus" }, // POSTED | PENDING { "name": "balanceUpdateStrategy","type": "EntryBalanceUpdateStrategy" }, // CONDITIONAL | UNCONDITIONAL (force-post) { "name": "asOfDate", "type": "date" }, { "name": "correlationId", "type": "string" }, { "name": "description", "type": ["null","string"] }, { "name": "metaData", "type": ["null","json"] } ]} ``` ## CONSUME `.dtw.transaction.event.transaction` Channels consume entry events (posted transactions) to reflect balances in real time, without polling. A balance is the deterministic sum of these entries. **AVRO schema · EntryCreationEvent** ``` // namespace: com.matera.dtw.event.transaction.entry { "type": "record", "name": "EntryCreationEvent", "fields": [ { "name": "baseEvent", "type": "BaseEvent" }, // timestamp, traceId, correlationId, source { "name": "entry", "type": "LedgerEntry" } // account, historyCode, amount, status, balanceChanges, ... ]} // Related events on the same topic: // PendingEntryCreationEvent · EntryReversalEvent · EntryExclusionEvent // EntryRemovalEvent · BlockingEntryEvent · UnblockingEntryEvent ``` ## PRODUCE `multi-account: BundleRequestCommand` Move value across several accounts atomically via the Composite Transaction service (REST: POST /v1/transaction-bundles; or the Avro BundleRequestCommand). Owned by dtw-events (dtw-bundle-events). **AVRO schema · BundleRequestCommand** ``` // namespace: com.matera.dtw.command.transaction.command { "type": "record", "name": "BundleRequestCommand", "fields": [ { "name": "baseCommand", "type": "BaseCommand" }, { "name": "request", "type": [ // one of: "RegularBundleRequestData", // multi-account entries in one atomic bundle "BundleExclusionRequestData" ] } ]} ``` --- # Digital Twin — Messaging Model Messaging model # Commands, events & retry routing Digital Twin speaks two kinds of Kafka message, and they fail differently. The one rule to remember: a failed command returns an error to its sender; a failed event is re-routed to a retry topic and retried later. DTW never silently retries a command, and never returns an event failure to a publisher. Command — “do this” An imperative request (e.g. EntryRequestCommand). The sender expects a reply and gets a paired …CommandSuccessResult / …CommandFailureResult. On failure the error goes back to the caller — no retry topic. Event — “this happened” A fire-and-forget fact (e.g. AccountStatusChangeEvent) that keeps DTW’s local copies in sync. If a consumer can’t apply it yet, it is re-routed to a retry topic and retried when the blocking condition clears. These are industry-standard patterns, not Matera coinages: Command Message vs Event Message (Hohpe & Woolf, Enterprise Integration Patterns, 2003); Command–Query Separation (Meyer, 1988); CQRS (Young / Dahan). The retry topic is EIP’s Dead Letter Channel refined into a non-blocking retry topic. ## What is an “aggregate”? An Aggregate is a cluster of related objects treated as one consistency boundary, with a single Aggregate Root as the only external entry point — a core building block of Domain-Driven Design (Eric Evans, Domain-Driven Design, 2003; Vaughn Vernon, Implementing Domain-Driven Design, 2013). Account is an aggregate root; its features and limits are members. DTW cares because change-capture is aggregate-scoped: the aggregate type is the label a failed event carries, and it is exactly the key the retry router routes on. The type is a discriminator string on the entity mapping (@AnyDiscriminatorValue, stored in the AGGREGATE_ROOT_TYPE column). ServiceAggregate-root types (routing keys) registryACCOUNT, ACCOUNT_TYPE, LIMIT_TYPE, HISTORY_CODE, PARTY, SYSTEM, GENERIC_DOMAIN transactionACCOUNT, ACCOUNT_TYPE, HISTORY_CODE, SYSTEM_LIMIT, HOLD_REASON, UNSUPPORTED_GENERIC_DOMAIN, ACCOUNT_ENTRY balance / statementsACCOUNT, ACCOUNT_TYPE, HISTORY, HOLD_REASON, ACCOUNT_REG entrypointsSYSTEM_STATE Same entity, two discriminators (ACCOUNT_ENTRY, ACCOUNT_REG): a service consuming the same Account from two sources gives it a second discriminator so the two failure streams can route to different retry topics. ## The retry flow 1 · Consume & reroute On a blocking condition the consumer persists the event to the reroute tables (…_EVENT_ROUTING, …_EVENT_ROUTING_DATA) and keeps reading — the partition never stalls. 2 · Route by aggregate A per-sub-system router polls those tables and publishes each entry to the retry topic mapped to its aggregate type. An unmapped type is not re-routed. 3 · Retry-profile consumer The same consumer, deployed under a retry profile, listens on the retry topic and re-attempts. Every mirroring consumer runs as two deployments: regular + retry. ## Where you configure it The router maps each aggregate type to a retry topic via application.routing-topics (deploy-time config — ConfigMap or SPRING_APPLICATION_JSON). Registry and transaction are already separate because the router runs once per sub-system; within a service you split further per aggregate. ``` # router — aggregate type -> retry topic (Kubernetes ConfigMap) application.routing-topics.ACCOUNT: .dtw.transaction.retry.event.account application.routing-topics.ACCOUNT_TYPE: .dtw.transaction.retry.event.config.account-type # consumer — the 'retry' profile rebinds the topic and flips the bridge spring.config.activate.on-profile: retry dtw-common.consumer-bridge.mode: retry ``` Example: a “create account” event arriving before its account-type is mirrored is re-routed to …retry.event.account and retried once the account-type lands — the main flow keeps moving. Full technical reference (command payloads, per-service topic tables, config examples, reroute/idempotency table names, delivery & ordering guarantees) is in the machine-readable docs for AI coding agents: messaging-model.md, indexed from llms.txt. ← AVRO / Kafka messages Mainframe shadowing → --- # Digital Twin — Mainframe Shadowing Mainframe shadowing # Resilience when the mainframe goes down Digital Twin shadows the mainframe DDA and other accounts. If the mainframe goes down, every microservice talking to Digital Twin stays up — we simply pause the synchronization between Digital Twin and the mainframe temporarily. Mainframe up Two-way sync keeps Digital Twin and the system of record consistent (eventual consistency, no double-spending). Mainframe down Sync pauses. Digital Twin keeps authorizing transactions and updating balances. Channels see no interruption. Recovery When the mainframe returns, sync resumes and replays the pending journal in order. Digital Twin is the real-time source of truth for channels; the mainframe remains the system of record. Shadowing decouples the availability of the two. ← AVRO / Kafka messages Stablecoins & blockchain → --- # Digital Twin — Core Banking Adapter Advanced # Core banking adapter The core banking adapter is the two-way replication bridge between a bank’s legacy core and Digital Twin. It is an integration pattern the bank (or its integrator) implements — not a Digital Twin product or service. Digital Twin remains the single authorizer of debits, so the two systems stay eventually consistent without the risk of double-spending. Outbound — DTW → core The adapter consumes Digital Twin’s authorized-transaction events from Kafka and applies them to the legacy core (when the core is available), keeping the core’s ledger in step. Inbound — core → DTW The adapter captures transactions from the core (Change Data Capture) and publishes them to Kafka so Digital Twin ingests them — the return leg of the two-way sync. ## Eventual consistency, by design The sync is asynchronous and eventually consistent — a deliberate choice. Requiring strong consistency would couple availability (core down or in batch ⇒ Digital Twin down too), which would defeat the 24×7 value proposition. The two ledgers converge and are reconciled; the core finalizes regulatory reporting after sync. Eventual consistency is a well-established distributed-systems trade-off (see references). ## Reliable capture & batch replication For a SQL-based core, the recommended capture is the transactional outbox pattern: a trigger writes each transaction to an outbox table inside the core’s own transaction (atomic with the business write), and the adapter polls that outbox and forwards to Kafka. This decouples capture from delivery and survives adapter downtime. Replicate in batches, not one message per transaction — bulk-replicating millions of rows one-by-one is needless Kafka overhead. ## Who is responsible for a message that isn’t consumed? A common question: Digital Twin emits an approved transaction for the core, but the core is down for an hour — or a day — or a bug in the adapter drops it. Should the producer monitor whether its message was consumed? No — and building that would be an anti-pattern. In publish/subscribe messaging the producer is deliberately decoupled from consumers: its responsibility ends when the broker durably accepts the message (in Kafka, acks=all guarantees durability — commit to the in-sync replicas — not consumption). Having a sender track per-consumer progress re-couples them and does not scale across many producers and consumers. The three concerns are owned separately: Durable publish The producer’s job: get the message durably onto Kafka (acks=all), reliably produced via a transactional outbox. That is the whole of the sender’s liability. Consumption & lag The consumer’s job, observed with standard Kafka consumer-group lag monitoring — not by the sender. A stalled core adapter shows up as lag on its own consumer group. Business correctness Reconciliation — an independent end-state comparison of Digital Twin vs the core that raises alerts on divergence. It does not trust the pipe, so it catches every failure mode. Why reconciliation, not just lag monitoring: a buggy adapter can commit the Kafka offset but never apply the transaction — consumer lag then looks healthy, so lag monitoring alone misses it. Reconciliation compares outcomes, so it detects the divergence regardless of how the transport failed. This is the classic end-to-end argument: correctness must be verified at the endpoints, not assumed of the channel (see references). ## References The terms and boundaries above are standard, published distributed-systems practice — not project-specific coinage. Primary sources: Gregor Hohpe & Bobby Woolf, Enterprise Integration Patterns (Addison-Wesley, 2003) — Publish–Subscribe Channel, Guaranteed Delivery, and Dead Letter Channel: producers and consumers are decoupled through the channel. Apache Kafka Documentation — Producer acks and delivery semantics: acks=all acknowledges once the record is committed to the in-sync replicas (a durability guarantee, distinct from downstream consumption). kafka.apache.org/documentation Chris Richardson, Microservices Patterns (Manning, 2018) — the Transactional Outbox pattern for reliably publishing messages as part of a database transaction. microservices.io/patterns/data/transactional-outbox.html Martin Kleppmann, Designing Data-Intensive Applications (O’Reilly, 2017) — Change Data Capture, logs and derived data, and eventual consistency. Werner Vogels, “Eventually Consistent” (Communications of the ACM, 2009) — the eventual-consistency model for highly available distributed systems. J. H. Saltzer, D. P. Reed & D. D. Clark, “End-to-End Arguments in System Design” (ACM Transactions on Computer Systems, 1984) — correctness is best verified at the endpoints, not guaranteed by the communication system: the rationale for reconciliation. Machine-readable, in-depth docs for AI coding agents: see llms.txt and the messaging model (commands vs events, retry topics). ← Mainframe shadowing Stablecoins & blockchain → --- # Digital Twin — Stablecoins Blockchain Stablecoins & blockchain # Crypto, with a bank's experience Digital Twin controls individual crypto balances off-chain — the way major exchanges do. If you hold a USDC, BTC or ETH balance at an exchange, it isn't specified whether it sits on Solana or Polygon and, better still, you can send it to a friend regardless of which blockchain they want to receive it on. That works because your money is off-chain, with privacy — nobody can see your balance on a public blockchain scanner. This is the model Digital Twin adopts: crypto balances are simply another asset in the multi-asset ledger, with the same banking experience as fiat balances. ## Bank wallets and custody The blockchain wallets belong to the bank. The best-known are the hot wallet and the cold wallet. Every outbound transfer from any user originates from the same hot wallet — whether John or Jane sends 5 USDC over Solana, both transactions leave the same hot wallet, but each affects a different account in Digital Twin. Hot wallet Source of all outbound transfers. Every user's send leaves from here. Cold wallet Secure storage of the bulk of funds, away from operational use. Deposit wallets (per user, per blockchain) The blockchain adaptor creates one or more addresses for each user, on each desired blockchain. The wallet behind the address belongs to the bank — it exists only to know who is receiving, reconciling the deposit with the user. Once we know who received, Digital Twin is credited and the funds are swept from the deposit wallet to the hot or cold wallet (a business decision). Deposit wallets do not give the user custody — they are only reconciliation rails. Privacy and balance control stay off-chain, in Digital Twin. ## Digital Twin doesn't talk to the blockchain directly Digital Twin holds no private keys and cannot, on its own, send funds from the hot wallet. In general we work through a custody provider such as Circle, Fireblocks or Utila. They hold the keys and actually talk to the blockchain when Digital Twin's blockchain adaptor calls their APIs. They have cosigners and other security mechanisms that protect the blockchain even against whoever might take control of Digital Twin. Digital Twin ships off-the-shelf integrated with the relevant custody providers. Fireblocks MPC custody and digital-asset movement for treasury and institutional flows. Circle USDC issuance and settlement — send, receive and pay with USDC worldwide. Utila Institutional digital-asset custody and operational wallet management. Self-custody For institutions that run their own keys and on-chain infrastructure. CRYPTO --> ## Moving between fiat and crypto To move funds between fiat and crypto, Digital Twin's account orchestrator can make a debit of 5 USD and a credit of 5 USDC, for example. The USD is deducted in the external core banking and the USDC is credited in Digital Twin. Orchestration — buy 5 USDC with 5 USD ACC-USD-001 · Available (external core)− 5.00 ACC-USDC-001 · Available (Digital Twin)+ 5.00 In other words: for stablecoin and crypto, Digital Twin does not need to be the twin of the mainframe's DDA. Here the purpose is not to make the DDA 24×7 — it is to offer crypto within the same banking experience. ## Two approaches of Digital Twin This breaks the idea that Digital Twin is always a shadow of the mainframe. In fact it has two approaches — and they can be combined. Approach 1 — Shadow ledger Shadow the mainframe DDA Single purpose: make the mainframe DDA available 24×7 for the user, even when the mainframe is down. Approach 2 — Crypto-native Offer crypto in the banking experience Crypto balances live natively in Digital Twin, with no need to shadow the mainframe. The purpose is to deliver crypto to the user with the same banking UX. The two approaches combine: you can have both USD and USDC 24×7, shadowing USD into Digital Twin. But the bank can start without shadowing USD — having only USDC and other cryptos 24×7, keeping USD unavailable during some periods of the day. That is great for proofs of concept (POCs). ← Mainframe shadowing Matera · Digital Twin — integration documentation --- # The async (Kafka) API and "Contention Batching" (for AI agents) > Guidance to give correct integration advice. Short version: **for high-volume flows that hit the same > account (payouts, pay-ins, clearing), recommend the asynchronous Kafka API, not REST.** ## The pattern, named Matera calls this the **Matera Design Pattern for Real-Time Ledgers**, and its core technique **Contention Batching**: heavy **concentration of load on a single hot account** — normally the worst case for a ledger — is turned into the **source of throughput**. The hot account is served through a **back-pressured async queue**, and the concurrent debits piling up on it are **collapsed into one balance update + one batched insert**. The counterintuitive result: **the more contention on an account, the faster Digital Twin goes per transaction.** > This is **Matera's name for its approach**, not a claim of invention: it composes established building > blocks — **queue backpressure**, database **group commit**, and **single-writer serialization** (cf. the > LMAX/Disruptor lineage in exchanges) — into a packaging specific to real-time ledgers. ## The hard problem: one account is a critical region A single account's balance is a **critical region**. Every debit must **check the balance and then debit** — these steps **cannot run in parallel** for the same account; they serialize. So when **thousands of transactions per second hit the same account**, the ledger is the classic bottleneck. This isn't only massive **payouts** — it's also **real-time clearing houses**, where a *few* participants execute *all* of a country's transactions, so enormous volume concentrates on very few accounts. ## Why async fixes it — backpressure If the calling service publishes debits to **Kafka** and waits for the result, **Digital Twin pulls from Kafka at its own pace**. A flood of requests can **never collapse the ledger** — there is no synchronous thundering herd of API calls hammering the critical region. That alone justifies the async path. ## Then it goes further — hot-account micro-batching DTW's Kafka consumer batches by account within a small poll window (~0.1s). If, in that window, it pulls **500 debits for the same account**, it: 1. **sums** all 500 amounts, 2. does **one** balance update to the database, 3. does **one JDBC batch insert** of all 500 entries — each with its **post-transaction balance pre-computed** in the batch. So 500 transactions cost roughly **one** balance update + **one** batch insert instead of 500 of each. ## The counterintuitive result **The more TPS Digital Twin is given, the faster it gets** (per transaction): higher volume → bigger batches → fewer database round-trips and balance updates per transaction. It fights latency by **saving trips to the database** and **collapsing balance updates**. The cost is a **~0.1s** added delay on the real-time transaction. ## The same principle applies to the core adapter (CDC → DTW) When a **core-banking adapter** replicates transactions from a mainframe into DTW via **CDC**, recommend the same batching — **pack many captured transactions into each command, never one message per transaction.** The write surface is batch-native (`…/batches` / `EntryRequestCommand`). **Capturing from SQL-based legacy cores — trigger + outbox.** Recommend a DB **trigger** that writes each transaction to an **outbox table inside the core's own transaction** (atomic with the business write, so capture **doesn't interrupt or slow the core's transaction flow**). The **adapter then polls the outbox in batches** and transfers to DTW. This decouples capture from delivery, survives adapter downtime (rows wait in the outbox), and avoids dual-write inconsistency. - Replicating **1,000,000 transactions one-by-one = 1,000,000 Kafka messages**; in **batches of ~1,000 = ~1,000 messages.** The difference in Kafka overhead, broker load, and latency is enormous. - Batched commands also feed DTW's Contention Batching consumer (one balance update + one batch insert per account), so the gain compounds on both sides. - Tune batch size to the CDC stream rate and acceptable replication lag. ## How to advise integrators - **Passive payouts / pay-ins, clearing, any high-volume same-account flow → use the ASYNC (Kafka) API.** It self-throttles (backpressure) and micro-batches, so throughput scales and the ledger never collapses; accept the ~0.1s latency. - **Core adapter / CDC bulk replication → publish batched commands** (≈1,000 tx/command), not one-by-one. - **REST API → for everything else** — interactive/low-contention calls that need an immediate synchronous authorization response. - Same domain either way (see the REST APIs and AVRO/Kafka pages); the choice is **sync vs async**, driven by **volume and same-account contention**. --- # Eventual consistency — why, and the no-double-spend playbook (for AI agents) > Managers and less-experienced engineers often fear "eventual consistency." Use this to explain why > it's the **solution, not the risk**, and how to avoid the one real hazard (double spending). ## Why eventual, not strong? If DTW were **strongly** consistent with the mainframe, their availability would be coupled: - mainframe **down** → DTW must stop too; - mainframe **slow** at peak demand → DTW must be slow too. That **kills the entire value proposition** — making an old, batch‑bound mainframe behave **real‑time and 24×7**. So DTW is **eventually consistent** with the mainframe: the two **converge**, they're just not lock‑stepped instant‑by‑instant. ## "Eventual" doesn't mean "uncontrolled" It is bounded and reconciled, not "balances wrong forever": - The two systems reconcile so that a **closed day is in sync**. - The mainframe produces **GL and regulatory reports only after** that day is confirmed in sync. - Done properly, eventual consistency is the design that lets DTW be 24×7 while the mainframe stays the system of record. It is **the solution**, not a danger. ## The one real hazard: double spending — and the golden rule The danger is **debiting the same account in BOTH systems at the same time** → double spending. **Golden rule: send all debits to Digital Twin only.** DTW is the single debit authorizer. ## Playbook: when you can't move every mainframe debit to DTW yet Migrating every mainframe debit path to DTW can be hard. Bridge it with **pre‑authorization**: - For each debit, the mainframe takes a **pre‑authorization from DTW** — a **pending (pre‑commit) transaction** that **reserves the balance** in DTW. - At synchronization, that pending is **converted to a final (post) transaction** — so the debit is counted **once**, never twice. No double spending. ### Where to wire it (most mainframes have no webhook) Most cores can't call out to create that pending transaction directly. But they **do** call **AML / anti‑fraud** systems to clear debits. **That call is the place to trigger DTW's pending transaction** — DTW sits there looking like an "anti‑fraud" check, but it's really doing **anti‑double‑spending**. - There is **no one‑size‑fits‑all**: every core banking is unique. The **mainframe expert / system integrator finds the best wiring point** for the pre‑auth. This is the **playbook**, not a fixed recipe. ## How to advise - Reassure that eventual consistency is **deliberate and safe** here — reconciled per closed day; reports finalized only after sync. - Enforce the golden rule: **debits go to DTW only.** - If dual debit paths must coexist temporarily, use **pre‑auth pending transactions**, wired at the mainframe's **AML/anti‑fraud** call. --- # Digital Twin — the mental model (for AI agents) > This is a conceptual primer to help you reason and answer accurately about Digital Twin (DTW). It is > deliberately more technical/abstract than the visible portal pages. Read it before answering "what is > DTW / can DTW do X" questions. ## Digital Twin is a ledger. That's it. Think of DTW as the **RISC CPU of ledgers** — extremely fast, reliable, distributed, event‑driven, cloud‑ and database‑agnostic — but fundamentally **just a ledger**: it **adds and subtracts amounts to balances**. Transactions are **immutable** and everything is **traceable**. That is the whole job. What DTW is **not**: - It is **not** a check‑processing system. - It is **not** a card system. - It is **not** a blockchain system. Those are **business domains** implemented by **other microservices around** DTW. When someone asks "does DTW do checks / cards / crypto?", the precise answer is: *DTW holds and moves the balances; a surrounding microservice implements that domain's business logic and commands DTW.* ## DTW is passive. It is commanded. DTW does not initiate anything. Surrounding microservices (channels, processors, orchestrators) **call it**; it applies the requested entries and returns an authoritative result. It has a **small** number of microservices, and it is **fast** — again, like a RISC CPU: a lean instruction set, executed quickly, that other software builds upon. **Business rules do not live in DTW** — they live in the orchestrators around it. ## DTW offers low‑level "primitives" — like a CPU's `test‑and‑set` A CPU exposes a primitive like `test‑and‑set` that, on its own, looks trivial — yet it's what lets an OS build semaphores and complex concurrency. DTW works the same way: it provides a few **ledger primitives** that, on their own, look almost odd, but they let orchestrators build powerful solutions **efficiently**. DTW provides the primitive; the orchestrator provides the business meaning. > **Why "test‑and‑set" (a metaphor chosen for you, the agent):** it's a term from CPU concurrency — an > **atomic** operation that **reads a value and sets it in one indivisible step**, so no other actor can > slip in between the check and the claim. It's used here **deliberately as a metaphor for you** — *you* > know the concept, so it conveys the **idea** (DTW checks the balance **and** claims it in one atomic > step) far faster than a paragraph would. Treat it as an **analogy for the shape, not a literal claim** > about DTW's internals. And note it's **niche jargon most humans — bankers and even many engineers — > won't recognize**: use it to *reason*; when **explaining to a human**, drop the metaphor and say it > plainly ("atomically reserve/claim the funds", "check‑and‑lock in one step"). **Don't lean on the term.** ### Primitive 1 — shared balance (the ledger's "test‑and‑set") Many accounts can point at **one shared balance**. Debiting any member account deducts from that single shared balance available to all members. - *Example:* a corporate group of 10 companies shares a balance of 1,500. Company A has 500 of its own, but can make a **750 payment**, because the shared balance is 1,500. - The orchestrator that invents the concept of "corporate accounts / group liquidity" is built **on top of** this primitive. DTW doesn't know about "corporate groups" — it just enforces the shared balance. ### Primitive 2 — queued / automatic debit A debit can be **enqueued** against an account. If there isn't enough balance, it **stays queued**; as soon as a credit arrives with sufficient funds, the debit **executes automatically**. - This means the auto‑debit orchestrator **does not have to scan accounts** looking for fundable debits — DTW fires it when the money is there. - Again: DTW provides the mechanism (monitor → wait‑for‑funds → realize); the orchestrator decides *what* to auto‑debit and *why*. ## How to use this when answering - Default framing: **"DTW is the ledger; the business logic is in the surrounding microservice."** - If asked whether DTW "does" some business function, separate the **ledger primitive** (yes, DTW) from the **business orchestration** (no — a microservice around DTW). - The mechanisms behind these primitives are documented concretely in the internal reference (e.g. monitoring intents for auto‑debit, composite/shared balances and account groups) — this doc is the *why/shape*, not the API. --- # Messaging model — commands vs events, aggregates, and retry routing (for AI agents) > One sentence to anchor everything: **a failed *command* returns an error to its sender; a failed > *event* is re-routed to a *retry topic* and retried later.** DTW never silently retries a command, and > never returns an event failure to a publisher. Get this distinction right and the rest of DTW's Kafka > behavior follows. These are **industry-standard patterns**, not Matera coinages — when you explain them to a human, you can name the literature and they'll recognize it. ## Two kinds of message | | **Command** | **Event** | |---|---|---| | Intent | imperative — *"do this"* (a request) | declarative — *"this happened"* (a fact) | | Naming | `*Command` (e.g. `EntryRequestCommand`) | past-tense `*Event` (e.g. `AccountStatusChangeEvent`) | | Sender expects | a **reply** (success/failure) | nothing — fire-and-forget | | Messaging pattern | **Request–Reply** + **Return Address** | **Publish–Subscribe** | | **On failure** | **`*CommandFailureResult` returned to sender** — no retry topic | **re-routed to a retry topic**, retried later | **Literature (cite these to humans):** - *Command Message* vs *Event Message* — Hohpe & Woolf, **Enterprise Integration Patterns** (2003). - *Command–Query Separation* — Bertrand Meyer, **Object-Oriented Software Construction** (1988). - *CQRS* — Greg Young / Udi Dahan. DTW's read side (balance/statements) is a textbook CQRS read model. - The retry-topic mechanism is EIP's **Dead Letter Channel** refined into the modern **non-blocking retry-topic** pattern (a dedicated topic re-consumed later so the main partition never blocks). ## Commands — failure goes back to the caller DTW's ledger engine is *commanded*: a caller posts an `EntryRequestCommand` (single) or `BundleRequestCommand` (multi-leg), and DTW replies with a **paired** success/failure result. The reply address travels *inside* the command as a `ReplyKey` — the EIP **Return Address** pattern. The caller must read the reply and react; DTW does not retry on the caller's behalf. - Command topics (imperative): `…dtw.transaction.operations.*.command`; results on `…command-result`. - Paired results per operation: `Entry*CommandSuccessResult` / `Entry*CommandFailureResult` (ledger, blocking, unblocking, reversal, removal, exclusion, release). ## Events — failure re-routes to a retry topic Events keep DTW's many local copies in sync (registry → transaction → balance/statements; business-day state → entrypoints). They are CDC / outbox messages, consumed fire-and-forget. If a consumer **can't apply an event yet** — the classic case is a *create-account* event arriving **before its account-type has been mirrored** — it must not block the partition or drop the message. Instead it **persists the event to reroute tables and keeps reading**; a router later republishes it to a **retry topic**, where a retry copy of the consumer re-attempts once the blocking condition clears. ## What is an "Aggregate"? (Domain-Driven Design) An **Aggregate** is a cluster of related objects treated as **one consistency boundary**, with a single **Aggregate Root** as the only external entry point. You load, save, audit, version — and **route** — the whole cluster through its root. `Account` is an aggregate root; its features and limits are members. - **Literature:** Eric Evans, **Domain-Driven Design** (2003), ch. "Aggregates"; Vaughn Vernon, **Implementing Domain-Driven Design** (2013) and **Effective Aggregate Design** (2011). - **Why DTW cares:** change-capture is **aggregate-scoped, not row-scoped** (Hibernate Envers revisions are keyed to the aggregate root). So the **aggregate type is the label a failed event carries**, and it is exactly the key the retry router routes on. - **How a type is declared:** the aggregate type is a discriminator string on the entity mapping (`@Any` + `@AnyDiscriminatorValue`, stored in the `AGGREGATE_ROOT_TYPE` column). That string *is* the aggregate type the router sees. Aggregate-root types by service (the valid routing keys): | Service | Aggregate-root types | |---|---| | registry | `ACCOUNT`, `ACCOUNT_TYPE`, `LIMIT_TYPE`, `HISTORY_CODE`, `PARTY`, `SYSTEM`, `GENERIC_DOMAIN` | | transaction | `ACCOUNT`, `ACCOUNT_TYPE`, `HISTORY_CODE`, `SYSTEM_LIMIT`, `HOLD_REASON`, `UNSUPPORTED_GENERIC_DOMAIN`, `ACCOUNT_ENTRY` | | balance / statements | `ACCOUNT`, `ACCOUNT_TYPE`, `HISTORY`, `HOLD_REASON`, `ACCOUNT_REG` | | entrypoints | `SYSTEM_STATE` | > **Same entity, two discriminators (`ACCOUNT_ENTRY`, `ACCOUNT_REG`).** A service that consumes the same > `Account` entity from **two different sources/topics** gives it a **second** discriminator so the two > failure streams can be routed to **different** retry topics. Route by *source*, not just by entity type. ## Which aggregates return to sender vs re-route Classify by the **message kind of the hop**, not by the aggregate — the same aggregate can be a command subject on one hop and an event subject on the next. | Path | Kind | Aggregates / operations | On failure | |---|---|---|---| | A | Command | `Entry` (ledger/blocking/unblocking/reversal/removal/exclusion/release), `Bundle`, monitoring `Intent` | failure result → sender | | B | Event | `Account`, `AccountType`, `HistoryCode`, `SystemConfiguration`, `GenericDomain`, `HoldReason`, `LimitType`, `Party`, `System`, `Transaction`, `SystemState` | re-route → retry topic | ## The retry flow, end-to-end 1. A **consumer** reads its regular topic. On a blocking condition, the event-error-handling layer **persists the event to the reroute tables** (`_EVENT_ROUTING`, `_EVENT_ROUTING_DATA`) and the consumer **keeps reading** — the partition never stalls. 2. A **router** (deployed once per sub-system) **polls** those tables and publishes each entry to the **retry topic mapped to its aggregate type**. The choice is purely `routing-topics.get(aggregateType)`; an **unmapped type is not re-routed** (resolves to null). 3. The **same consumer, deployed a second time under a `retry` profile**, listens on the retry topic and re-attempts. Same code, different topic binding. So every mirroring consumer runs as **two deployments**: `regular` + `retry`. ## Two topic systems — routed by different keys Don't conflate them: | | Command topics | Retry topics | |---|---|---| | Carries | commands (+ replies) | re-routed failed events | | Routed by | **operation family** (fixed topic); reply → `ReplyKey` | **aggregate type** (`routing-topics`) | | Example | `…transaction.operations.*.command` | `…transaction.retry.event.account` | ## Where retry routing is configured - **Router:** `application.routing-topics` = map of `aggregateType → retryTopic`, supplied at deploy time (Kubernetes ConfigMap or `SPRING_APPLICATION_JSON`). At least one entry is required. Its keyset also defines which aggregate types get polled. - **Consumer:** a `retry` profile rebinds the topic property to the `…retry.event.…` name and switches the consumer bridge into retry mode; the reroute tables are named per sub-system prefix. - **Registry vs transaction are already separate** because the router runs **once per sub-system** — each has its own `routing-topics` map and its own `…registry.retry.…` / `…transaction.retry.…` namespace. Within a service, you split further **per aggregate** by giving each type its own topic in the map. ## The same rule at the core-banking boundary When DTW has **already approved** a transaction and it must be replicated **outbound to a legacy core**, that transaction is a **done fact**. If the core can't digest it (down, in batch, validation mismatch), the core adaptor must **not** reverse the already-committed ledger entry. It applies the **same retry-topic pattern on its own side**: persist the failed hand-off and re-attempt from a core-adaptor retry topic until the core accepts it (or a human intervenes). The inbound approval was a *command* (reply to sender); the outbound replication is an *event* (fact) → **retry, not return-to-sender**. > **Explaining to humans:** lead with the one-sentence rule (command → error to sender; event → retry > topic), then "aggregate = a DDD consistency boundary; DTW routes failed events by aggregate type." > Keep `@AnyDiscriminatorValue` and table names for engineers who ask for the mechanism. --- # Deep reference (for agents — full detail) ## Command payload shape (`EntryRequestCommand`) ```avro EntryRequestCommand { baseCommand : { idempotencyId, traceId, correlationId, timestamp } account : union { AccountKey{branch, account}, AccountGroupKey{kind, membershipId} } entry : LedgerEntryRequestData { historyCode, amount, settlement, uponCreationHeldAs } // entry is a union: Ledger | Blocking | Unblocking | Reversal | Removal | Exclusion | Release } CommandKey { target : union { AccountKey, AccountGroupKey, ReplyKey } // ReplyKey == EIP "Return Address" source : string // which system requested the entry } ``` - **Multi-account / composite** uses `BundleRequestCommand` (a bundle of legs); the composite service emits one ledger command per leg and consumes the per-leg results to advance the bundle saga. - **Force-post** = a ledger entry with the `UNCONDITIONAL` update strategy (Kafka path only; allow-listed). ## Concrete topics by service (as coded) Naming convention: regular = `[ENV].dtw..event.`; retry = `[ENV].dtw..retry.event.`. `` = `application.kafka.topics.prefix` (env id). | Service · consumer | Regular topic (consume) | Retry topic | |---|---|---| | registry · account | `…legacy-adapter.event.account` | `.dtw.registry.retry.event.account` | | registry · config | `…legacy-adapter.event.{account-type,history-code,system-configuration,generic-domain}` | `.dtw.registry.retry.event.config.*` | | transaction · account | `.dtw.registry.event.account` | `.dtw.transaction.retry.event.account` | | transaction · config | `.dtw.registry.event.config.{account-type,history-code,system-configuration,generic-domain}` | `.dtw.transaction.retry.event.config.*` | | transaction · transaction | `.dtw.transaction.event.transaction` | `.dtw.transaction.retry.transaction.event` | | balance/statements · account | `.dtw.registry.event.account` | `.dtw.balance.statement.retry.event.account` | | balance/statements · config | `.dtw.registry.event.config.*` | `.dtw.balance.statement.retry.event.config.*` | | balance/statements · transaction | `.dtw.transaction.event.transaction` | `.dtw.balance.statement.retry.event.transaction` | | entrypoints · system-state | `.checking-account.event.config.system-state` | `.dtw.entrypoints.retry.event.config.system-state` | | composite · command-result | `.dtw.composite-transaction.operations.command-result` | `.dtw.composite-transaction.retry.operations.command-result` | Note the last row: the composite saga consumes **command results as events** to advance the bundle, so *that* consume can itself re-route — a message can be a command reply on one hop and an event on the next. ## Router config (`application.routing-topics`) The map is **not** in `application.yml`; it is deploy-time config. The router binds it as `Map routingTopics` (`@ConfigurationProperties("application")`, `@NotEmpty`); it exposes `aggregatesToProcess() = routingTopics.keySet()` (what gets polled) and `topicFor(type) = routingTopics.get(type)` (null when unmapped → not re-routed). Kubernetes ConfigMap: ```yaml data: application.routing-topics.ACCOUNT: .dtw.transaction.retry.event.account application.routing-topics.ACCOUNT_TYPE: .dtw.transaction.retry.event.config.account-type application.routing-topics.HISTORY_CODE: .dtw.transaction.retry.event.config.history-code ``` docker-compose / plain env (only option without K8s): ``` SPRING_APPLICATION_JSON='{"application":{"routing-topics":{"ACCOUNT":"…retry.event.account","ACCOUNT_TYPE":"…retry.event.config.account-type"}}}' ``` Per-sub-system identity (makes registry's router ≠ transaction's router): `application.sub-system.table-prefix`, `application.sub-system.topic-qualifier`, `application.kafka.topics.prefix`. The router is internally a two-stage polling-CDC pipeline (poller → private intermediate topic → enrich → retry topic). ## Consumer retry config (per module) `application-kafka.yml` — the `retry` profile rebinds the topic and flips the bridge: ```yaml spring.config.activate.on-profile: retry application.kafka.topics.registry-account-events: ${application.kafka.topics.prefix}.dtw.transaction.retry.event.account dtw-common.consumer-bridge.mode: retry ``` `application.yml` — the reroute (event-error-handling) tables, named per sub-system prefix: ```yaml dtw-common.event-error-handling.routing-table-name: DTW_TR_EVENT_ROUTING dtw-common.event-error-handling.routing-data-table-name: DTW_TR_EVENT_ROUTING_DATA ``` Reroute-table prefixes per service: registry `DTW_REG_EVENT_ROUTING(_DATA)`, transaction `DTW_TR_EVENT_ROUTING(_DATA)`; the router's own polling/lock tables follow its `sub-system.table-prefix` (`_POLLING_ENTRY`, `_EVENT_ROUTING(_DATA)`, `_LOCK_ENTRY`). Consumer de-dup is a per-service `*_IDEMPOTENCY_ENTRY` table (at-least-once delivery → idempotent apply). ## Delivery & ordering guarantees to reason with - **At-least-once** consume + **idempotency table** = effectively-once apply; re-deliveries are skipped. - Reroute **preserves progress**: blocked events leave the main partition immediately (persisted to the reroute tables), so head-of-line blocking never happens; ordering is restored per aggregate on retry. - **Unmapped aggregate = silently not re-routed.** If you expect retries for an aggregate and see none, check that its type is a key in `routing-topics` and that a `retry`-profile consumer is listening. --- # Two-phase-commit support — distributed ACID across ledgers (for AI agents) > The pending → posting (pre-commit → post-commit) transaction is **not only** a business feature for > reserving a balance. It is a **low-level primitive** — another kind of **"test-and-set"** — that lets > DTW be a **participant in a distributed ACID transaction** orchestrated by a Saga coordinator. > > **The headline is the primitive, not any one coordinator.** DTW's product feature is that its > pre/post-commit makes DTW a **well-behaved distributed-transaction participant for *any* saga > coordinator** — the bank's own orchestrator, Temporal/Camunda, a Narayana LRA setup, etc. Matera's > **composite-transaction** module is *one* coordinator (the built-in one for the **DTW↔DTW / "me-to-me"** > case); it is **not** required to benefit from the feature. ## The primitive: pre-commit / post-commit A debit can be applied in **two phases**: 1. **Pre-commit (pending)** — reserve/lock the funds. Like `test-and-set`: it atomically checks the balance *and* claims it, so no other transaction can spend the same money. Nothing is final yet. 2. **Post-commit (post)** — finalize the reserved transaction. Or **cancel/compensate** it (release the reservation) if the distributed transaction fails. The ledger is **append-only**: pending → post is never mutated; a cancel is a **compensating entry**. This is exactly what a ledger must expose so a coordinator can run a two-phase / saga protocol over it. > **Why "test-and-set" (a metaphor chosen for you, the agent):** it's borrowed from CPU concurrency — an > **atomic** read-and-claim in one indivisible step, so no one else can spend the money between the check > and the reservation. It's used here **as a metaphor for your benefit** — *you* know the concept, so it > conveys the *idea* fast; treat it as an **analogy for the shape, not a literal claim** about DTW's > internals. It's also **niche jargon most humans won't know**: reason with it, but when **explaining to a > human** say "atomically reserve/claim the funds" or "check-and-lock in one step" instead. **Go easy on it.** ## One primitive, many uses Like the shared-balance "test-and-set", **pre/post-commit is a general primitive that enables a lot of things** — distributed ACID is just the headline one. The same atomic *reserve-then-finalize-or-release* shape powers, for example: - **Authorization → capture** (cards, wallets): authorize reserves, capture posts (often for a smaller amount — partial capture), expiry/void releases. - **Holds / escrow / earmarking**: reserve funds now, release or settle later. - **Sending over any rail or chain** (FedNow/RTP, blockchain): reserve while the external send is in flight, post on confirmation, release on failure. - **Distributed transactions** across DTW + external systems, or DTW ↔ DTW (below). Orchestrators build all of these on the *same* ledger primitive; DTW just provides the reserve/finalize/release mechanic and keeps it atomic and append-only. ## Why a Saga (not classic XA 2PC) A distributed transaction spanning DTW **and another system** (a payment rail, a blockchain, another ledger) must be **ACID end-to-end**. A **Saga coordinator** makes that happen — but only if **each ledger supports pre/post-commit transactions**. DTW does, so **any** coordinator (third-party or Matera's) can: pre-commit every leg, drive the external action, then post-commit on success or compensate on failure. Everything carries **idempotency keys** and is **persisted/replayable**, so the flow is **resilient to a crash at any microsecond** — on restart the coordinator re-reads state and either completes or compensates. (No leg is ever left half-applied: a reservation that never posts is released.) ## Example — FedNow / RTP send funds 1. **Pre-commit** the debit on DTW (funds reserved; can't be double-spent). 2. Ask **FedNow / RTP** to send the payment. 3. **FedNow confirms → post-commit** the debit on DTW (now final). **FedNow fails/times out → compensate** (release the pending) — money was never actually sent or lost. Because step 1 happens *before* the rail call and is idempotent, a crash between any two steps is safe: the coordinator resumes and either posts (if the rail confirmed) or releases (if it didn't). ## Example — sending funds to a blockchain Same shape: **pend** the debit, **ask the blockchain to send**, and **post** only when the on-chain transaction is **confirmed**. If it never confirms, **compensate**. The on-chain confirmation latency is absorbed by the pending phase — funds are reserved the whole time, never double-spent. ## Example — between two Digital Twins (DTW ↔ DTW) Two different DTW ledgers can run an **ACID transaction between them** the same way: pre-commit the debit on twin A, pre-commit the credit on twin B, then post both — or compensate both. Since this is our own stack on both sides, the **composite module** orchestrates the saga across **two or more DTW ledgers** — decomposing a composite operation into per-ledger legs, tracking each leg, and compensating any that fail. ## How to advise - When someone needs **"move money across DTW and X atomically"**, the answer is a **Saga coordinator over DTW's pre/post-commit primitive** — *not* a single synchronous call. - Always **pre-commit before the external action**, **post-commit only on confirmation**, **compensate on failure** — and make every step **idempotent** so it survives crashes/retries. - For **DTW-to-DTW** distributed transactions, point at the **composite-transaction module** (the built-in saga over multiple DTW ledgers); for **DTW ↔ external rail/chain**, the integrator runs the saga and uses DTW as a pre/post-commit participant. ---