Integrate the sandbox into a TPP demo journey

A guide for fintech engineers who already have a data-sharing journey in their app — wired to the Nebras-operated regulatory sandbox for conformance — and want richer, narratively-coherent payloads for sales decks, investor pitches, regulator demos, and internal QA.

Disclaimer. Synthetic, illustrative data. Not endorsed by Nebras, CBUAE, or any LFI. The Median populate-rate is a Commons-curated ecosystem assumption, not a substitute for the authoritative test data surface that the Nebras-operated regulatory sandbox provides at certification time. Use these fixtures for showcase journeys, not for compliance attestation.

What this gets you

Pick your plug point

If you are…Use…
In a browser demo and want to render a persona inside an iframe (sales deck, blog, LMS) Path 1 — iframe embed
In Node / TypeScript and want to swap your Nebras-mock backend Path 2 — npm package
In Python (notebook, FastAPI mock, ML pipeline) Path 3 — PyPI package
In Swift / Kotlin / Postman / curl / Flutter / .NET Path 4 — raw HTTPS fixtures
Driving Claude (Desktop, Code, claude.ai) as a dynamic PFM against a synthetic customer Path 5 — MCP connector
Building a multi-bank reconciliation flow (UAE accounting-platform integration) Any path + multi-LFI consent flow (D-14)

Path 1 — iframe embed

EXP-27 ships a chrome-less variant of the sandbox you can drop into any HTML host. Use the Use this persona in your demo panel on a persona page to copy a pre-filled snippet, or build the URL yourself:

<iframe
  src="https://openfinance-os.org/commons/data-sandbox/embed.html?persona=salaried_expat_mid&lfi=median&endpoint=/accounts/%7BAccountId%7D/transactions&seed=4729&height=600"
  width="100%" height="600" loading="lazy"
  title="Open Finance Data Sandbox — Sara · median"
  referrerpolicy="no-referrer"
  style="border:1px solid #d9d5cb;border-radius:4px"></iframe>

Parameters

Path 2 — npm loadJourney()

The @openfinance-os/sandbox-fixtures package (EXP-20) bundles every fixture, the parsed v2.1 spec, and a small loader. The loadJourney() helper (EXP-29) returns the full coherent bundle for one tuple in a single call — drop it in where your TPP demo currently fetches from the Nebras-operated regulatory sandbox.

npm install @openfinance-os/sandbox-fixtures
// Wrap sandbox fixtures behind a Nebras-mock-shaped Express handler.
// Your existing fetch-from-Nebras code keeps working unmodified.
import express from 'express';
import { loadJourney } from '@openfinance-os/sandbox-fixtures';

const app = express();
const journey = loadJourney({
  persona: 'salaried_expat_mid',
  lfi: 'median',
  // seed: 4729,   // optional — defaults to the persona's default_seed
});

app.get('/open-finance/account-information/v2.1/accounts',
  (_req, res) => res.json(journey.endpoints['/accounts']));

app.get('/open-finance/account-information/v2.1/accounts/:id/transactions',
  (req, res) => res.json(journey.endpoints[`/accounts/${req.params.id}/transactions`]));

app.get('/open-finance/account-information/v2.1/parties',
  (_req, res) => res.json(journey.endpoints['/parties']));

app.listen(3000);

journey.accountIds tells you which IDs your demo can reference; journey.customerId is the calling-party id for /parties. journey.specSha and journey.version are your pin points.

Path 3 — PyPI load_journey()

Same data, same determinism, same coherence guarantees — Python edition.

pip install openfinance-os-sandbox-fixtures
# A FastAPI shim that mirrors the v2.1 Account Information surface
# but serves persona-coherent sandbox data.
from fastapi import FastAPI
from openfinance_os_sandbox_fixtures import load_journey

app = FastAPI()
j = load_journey("hnw_multicurrency", lfi="rich")  # Layla — multi-currency / FX-heavy

@app.get("/open-finance/account-information/v2.1/accounts")
def accounts():
    return j["endpoints"]["/accounts"]

@app.get("/open-finance/account-information/v2.1/accounts/{account_id}/balances")
def balances(account_id: str):
    return j["endpoints"][f"/accounts/{account_id}/balances"]

@app.get("/open-finance/account-information/v2.1/parties")
def parties():
    return j["endpoints"]["/parties"]

Path 4 — raw HTTPS fixtures

Fixtures are also published as static JSON at a stable, CORS-permissive URL (EXP-28). Anything that speaks HTTP can consume them — Swift, Kotlin, Flutter, .NET, Postman, plain curl.

URL contract

https://openfinance-os.org/commons/data-sandbox/fixtures/v1/manifest.json
https://openfinance-os.org/commons/data-sandbox/fixtures/v1/spec.json
https://openfinance-os.org/commons/data-sandbox/fixtures/v1/index.json
https://openfinance-os.org/commons/data-sandbox/fixtures/v1/personas/<persona>.json
https://openfinance-os.org/commons/data-sandbox/fixtures/v1/bundles/<persona>/<lfi>/seed-<n>/<endpoint>.json

Endpoint filenames replace / with __ and strip {/}. Examples:

Discovery

Start with index.json — a TPP-friendly summary of which personas, LFIs, endpoints, and version are live, plus the path contract:

curl -fsS https://openfinance-os.org/commons/data-sandbox/fixtures/v1/index.json

Postman walk-through

  1. GET .../fixtures/v1/manifest.json — capture fixtures["<persona>|median|<seed>"].accountIds[0] into a Postman environment variable {{accountId}}.
  2. GET .../fixtures/v1/bundles/<persona>/median/seed-<n>/accounts.json.
  3. GET .../fixtures/v1/bundles/<persona>/median/seed-<n>/accounts__{{accountId}}__balances.json — note the doubly-encoded curlies are fine in raw URLs because the on-disk filenames don't actually use {/}; resolve the variable, then fetch the resolved path.

CORS

/fixtures/v1/* ships Access-Control-Allow-Origin: *, so a browser-hosted TPP demo on a different origin can fetch directly. No auth, no cookies, no consent flow — these are static JSON files. If your org requires self-hosting, install the npm or PyPI package and ship the fixtures inside your bundle.

Path 5 — MCP connector (Claude)

PRD D-13 ships @openfinance-os/sandbox-mcp — a Model Context Protocol server that wraps the same fixtures as MCP tools, resources, and prompts. The intended use is running Claude as a dynamic PFM against a synthetic customer: pick a persona with set_session and let Claude answer balance, spend, and obligation questions over deterministic v2.1 JSON. Banking domain only; read-only; anonymous.

Hosted HTTP (Claude.ai connector / Claude Code)

Live endpoint: https://data-sandbox.fly.dev/mcp (canonical CNAME https://mcp.openfinance-os.org/mcp lands during the OF-OS Commons cutover). No auth, no API key.

Local stdio (Claude Desktop / npx)

{
  "mcpServers": {
    "open-finance-sandbox": {
      "command": "npx",
      "args": ["-y", "@openfinance-os/sandbox-mcp"]
    }
  }
}

Every tool response carries the same _watermark, _specSha, and _retrievedAt as the other plug-points (EXP-19 / EXP-10). Determinism (EXP-05) holds: the same (persona, lfi, seed) returns byte-identical bundles whether the client reaches it via npm, raw HTTPS, or MCP. See the package README for the full tool list (get_party, get_accounts, get_balances, get_transactions, …) and the build_persona custom-recipe path.

Multi-LFI consent flow (D-14)

UAE SMEs are typically multi-banked — operating account at a Tier-1 conventional bank, Islamic deposit / savings at a second LFI, and increasingly a digital-challenger relationship for SME tooling. A UAE accounting-system integration consumes feeds from all of these in parallel and reconciles them into one ledger. The sandbox surfaces this multi-bank reality at three layers, every layer spec-validated against v2.1.

1. Discoverability — what the persona declares

Every persona's manifest entry in /fixtures/v1/manifest.json#personas[<id>] carries an optional multi_lfi_footprint block:

{
  "name": "<persona display name>",
  "domain": "banking",
  "default_seed": 5172,
  "stress_coverage": ["aggregator_settlement_reconciliation"],
  "multi_lfi_footprint": {
    "primary":   { "role": "operating",          "lfi_default": "Rich",   "plausible_lfi_candidates": [/* real UAE Tier-1 bank names — see manifest.json */] },
    "secondary": { "role": "acquiring",          "lfi_default": "Median", "plausible_lfi_candidates": [/* card-acquiring banks */] },
    "tertiary":  { "role": "digital_challenger", "lfi_default": "Sparse", "plausible_lfi_candidates": [/* digital-only CBUAE-licensed banks */] }
  }
}

Real UAE bank names appear inside plausible_lfi_candidates in the live manifest per PRD decision D-14: a candidate set with no populate-rate binding is descriptive of the persona's banking reality, not an operational claim about any named bank. The lfi_default populate-rate label (Rich / Median / Sparse) stays anonymous. The MCP list_personas tool surfaces the same block plus an available_lfi_roles list of slots that actually have role bundles emitted (some declared slots resolve to candidates that aren't deposit-taking banks and are silently dropped).

2. Role-keyed bundles (Phase D Slice 5)

For personas with multi_lfi_footprint, the sandbox emits separate v2.1-shaped bundles for each declared non-primary slot at a role-keyed stage path. The primary stays at the historical URL (D-11 forward-compat); secondary / tertiary land at:

/fixtures/v1/bundles/<persona>/<slot>/<lfi>/seed-N/accounts.json
/fixtures/v1/bundles/<persona>/<slot>/<lfi>/seed-N/accounts__<AccountId>__balances.json
/fixtures/v1/bundles/<persona>/<slot>/<lfi>/seed-N/accounts__<AccountId>__transactions.json
... (and the rest of the v2.1 in-scope endpoints, single-account)

The role-bundle's /accounts envelope contains a single Account at the slot's deterministically-picked bank; its IBAN byte-matches the corresponding self-to-<slot> beneficiary's CreditorAccount.Identification in the primary bundle's /accounts/{AccountId}/beneficiaries. That match closes the cross-bundle reference loop:

// In the primary's beneficiaries:
{
  "Reference": "self-to-tertiary",
  "AccountHolderName": "<persona organisation legal name>",
  "CreditorAgent": { "SchemeName": "BICFI", "Identification": "<bank BIC>", "Name": "<bank name>" },
  "CreditorAccount": [{ "SchemeName": "IBAN",
                        "Identification": "AE228302598803958486047",   // ←─┐
                        "Name": "<persona organisation legal name>" }] //   │
}                                                                       //   │ MATCH
                                                                        //   │
// In the tertiary bundle's /accounts:                                  //   │
{ "AccountIdentifiers": [{ "SchemeName": "IBAN",                       //   │
                            "Identification": "AE228302598803958486047" }] }//←┘

Manifest registry: manifest.json#roleFixtures indexes every role bundle by "<persona>|<slot>|<lfi>|<seed>". npm / PyPI loaders accept an optional lfi_role: 'secondary'|'tertiary' param on loadFixture() and loadJourney(); the new listRoleBundles(personaId) helper enumerates the slots a persona has emitted bundles for.

3. Cross-LFI mirror ledger (Slice 7)

For every (persona, lfi, seed) with multi_lfi_footprint, the primary bundle's transaction stream carries 12 monthly self-sweep outflows per declared non-primary slot, byte-mirrored as inflows in the corresponding role bundle's /accounts/{AccountId}/transactions. Each pair shares TransactionDateTime, Amount, Currency, TransactionReference (XLFI-<sN>-NN), plus cross-pointers via CreditorAccount.Identification on the primary side and DebtorAccount.Identification on the role side — both mod-97-valid IBANs whose identity proves "same persona, two banks." A TPP can join by TransactionId stem (<persona>-xlfi-<sN>-NN-<dir>) which survives any LFI populate-rate profile, or by (TransactionDateTime, Amount, Currency) under Sparse where counterparty fields are stripped.

MCP plug-point — pin a role at runtime

An LLM-driven integration calls set_session({ persona, lfi, lfi_role: 'secondary' }) on the @openfinance-os/sandbox-mcp connector and gets the role bundle's slice for every subsequent get_* tool — get_accounts, get_balances, get_transactions, load_journey all route to the role-keyed manifest entry. Switching slots is a fresh set_session call.

Worked example with running code: examples/accounting-multi-bank-demo/.

Cross-endpoint coherence

Within a single (persona, lfi, seed) tuple, every AccountId in /accounts resolves to a matching /accounts/{AccountId}/balances, /accounts/{AccountId}/transactions, /accounts/{AccountId}/standing-orders, /accounts/{AccountId}/direct-debits, /accounts/{AccountId}/beneficiaries, /accounts/{AccountId}/scheduled-payments, /accounts/{AccountId}/parties, /accounts/{AccountId}/product, and /accounts/{AccountId}/statements; the calling PartyId in /parties is consistent across the bundle. EXP-32 enforces this in CI on every release.

This is the property the Nebras-operated regulatory sandbox does not optimise for (intentionally — it is a conformance environment). It is the reason a TPP showcase journey looks empty against Nebras mocks but looks like a real customer against sandbox personas.

Determinism & pinning

Every fixture is a pure function of (persona, lfi, seed, build-time now-anchor). To make your demo reproducible across a sales-cycle:

Stability contract

Path slot
/fixtures/v1/ — major-version boundary
Within v1
Additive only: new personas, new endpoints, in-v2.1 spec-SHA bumps, populate-rate evolution.
Breaking change
New /v2/ slot + new package major. Old /v1/ stays live for ≥ 24 months (PRD §16).
Pin point (immutable)
npm/PyPI package version
Pin point (mutable, latest-only)
manifest.json.version on /fixtures/v1/manifest.json
Spec drift signal
manifest.json.specSha — bumps within 30 days of upstream Nebras releases on the ozone branch.

Common pitfalls

Worked example

A single-page faux budgeting widget that consumes the raw HTTPS fixture URLs and renders a coherent account list, balance summary, transaction timeline, and fixed-commitments tile. Includes a Postman collection that chains {{accountId}} from /accounts into the per-account requests.

Live spec pin

Standards baseline
UAE Open Finance v2.1
Pinned spec SHA
Retrieved
Sandbox version
Generated at

Reporting issues

Found a coherence bug, a populate-rate band you disagree with, or a Nebras-mock-shaped surface you wish the sandbox covered? File at github.com/openfinance-os/data-sandbox/issues. Triaged within 14 days.