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.
What this gets you
- 38 narratively-coherent UAE personas — 21
banking-only (retail, SME, corporate), 9 insurance-only (motor / home
/ health / life / travel / renters / employment), and 8 multi-domain
personas whose
domains: [banking, insurance]declaration emits both endpoint families at the same persona path. The banking tab in the sandbox UI renders 29 personas (21 banking-only + 8 multi-domain); the insurance tab renders 17 (9 insurance-only + 8 multi-domain). Each persona stresses a distinct spec area (EXP-25); the flagshipretail-multi-bankerexercises N-slot multi-LFI banking (4 LFIs × multiple products) plus multi-insurer coverage in a single connect journey. - 3 LFI populate-rate profiles per persona — Rich (most optionals populated), Median (Commons-curated assumption), Sparse (worst-case mandatory-only). Mandatory fields are never redacted by the profile filter; only optional and conditional fields move.
- 12 v2.1 Account Information endpoints per (persona × LFI):
/accounts,/accounts/{AccountId},/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,/accounts/{AccountId}/statements,/parties. - Cross-endpoint coherence — for one
(persona, lfi, seed)tuple, AccountIds, the callingPartyId, transaction account-references and standing-order / direct-debit links all resolve consistently across the bundle. Verified per build by EXP-32. - Determinism — same
(persona, lfi, seed)always yields byte-identical envelopes (EXP-05). Pin the seed, pin the package, and your pitch demo runs the same way every time. - Watermarks, in every export and envelope — every fixture
carries
_persona,_lfi,_seed,_specSha,_retrievedAtand a textual watermark line. Carry these through screenshots and recordings.
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
persona— persona id (see the library).lfi—rich,median(default), orsparse.endpoint— any v2.1 path string, including templated{AccountId}form.seed— integer; defaults to the persona'sdefault_seed. Pin it for reproducible pitches.height— optional, in pixels; sets the iframe host height.
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:
/accounts→accounts.json/parties→parties.json/accounts/{AccountId}/transactions→accounts__AccountId__transactions.json
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
GET .../fixtures/v1/manifest.json— capturefixtures["<persona>|median|<seed>"].accountIds[0]into a Postman environment variable{{accountId}}.GET .../fixtures/v1/bundles/<persona>/median/seed-<n>/accounts.json.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.
- Claude.ai — Settings → Connectors → Add custom connector → paste the URL.
- Claude Code:
claude mcp add --transport http open-finance-sandbox https://data-sandbox.fly.dev/mcp
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:
- npm / PyPI: pin the package version
(
"@openfinance-os/sandbox-fixtures": "1.x.y"oropenfinance-os-sandbox-fixtures==1.x.y). Versions are immutable. - raw HTTPS: read
manifest.json.versionat boot and snapshot if it changes. The/fixtures/v1/path is latest-only within v1. - Either way: pass an explicit
seedin yourloadJourney()/load_journey()call, or rely on the persona'sdefault_seedand live with the assumption that the default never changes within a major version.
Stability contract
Common pitfalls
- Populate rates are illustrative, not authoritative. Median is a Commons-curated assumption — do not cite it as "Open Finance UAE data shows X% coverage". Use Sparse to stress-test your worst-case UI; use Rich to stress-test your best-case parser.
- Watermarks must travel through screenshots and recordings.
Every envelope carries
_watermark; every export carries the same line in CSV/JSON/tarball form. Don't crop it out. - Currency is AED-only by default. Use the
hnw_multicurrencypersona to exercise multi-currency flows. - Treat field gaps as a feature. Sparse-profile bundles
ship the mandatory-only shape. If your UI breaks because
MerchantCategoryCodeis missing, that's the bug it found. - This is not a TPP runtime. Sandbox fixtures do not substitute for Nebras-sandbox conformance. Use them for showcase, not certification.
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.
- ▶ Run the live demo
- Source on GitHub (
examples/tpp-budgeting-demo/)
Live spec pin
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.