A backend-first fintech infrastructure prototype exploring payments, wallets, ledgers, FX conversion, KYC/AML compliance, CBDC bridge infrastructure, ledger/rail reconciliation, and a settlement engine.
Payment Bridge Core is a fintech infrastructure prototype that models how modern payment systems work under the hood — from JWT auth and wallet management through FX conversion, compliance rules, CBDC settlement via ISO 20022 messaging, reconciliation between the internal ledger and external rails, and a settlement engine that separates "the rail accepted this" from "the money is actually final."
The goal is not to build a production bank.
The goal is to understand the engineering concepts, system boundaries, and architectural decisions that power modern financial infrastructure — including the emerging CBDC ecosystem and the reconciliation/settlement discipline that keeps a ledger trustworthy.
Most developers integrate payment APIs without seeing the infrastructure that sits behind them.
This project explores:
- payment processing workflows and lifecycle
- wallet and balance management (ledger-backed)
- double-entry ledger recording
- payment rail abstraction
- FX conversion and multi-currency support
- KYC level enforcement and AML pattern detection
- CBDC bridge: MINT, REDEEM, TRANSFER, SWAP operations
- ISO 20022 messaging standard (pacs.008 / pacs.002)
- reconciliation between local ledger state and external rail state
- settlement as a distinct step between rail acceptance and ledger finality
- role-based access control for administrative operations
| Category | Technology |
|---|---|
| Language | Java 21 |
| Framework | Spring Boot 3.5.x |
| Security | Spring Security + JWT |
| Database | H2 (in-memory) |
| Migration | Flyway |
| Documentation | OpenAPI / Swagger |
| Testing | JUnit 5, Mockito |
User
│
▼
Auth & JWT (role: USER | ADMIN)
│
▼
Wallet (balance from ledger)
│
▼
Payment Service
│
├── KYC Check (level-based tx limit)
├── AML Check (daily limit + structuring)
│
├── FX Service
│ ├── MockFxRateProvider (default)
│ └── ExchangeRatesApiFxProvider (real)
│
▼
RailRouter
├── MockRail (local testing)
├── StripeRail (Stripe sandbox)
└── CbdcRail (ISO 20022)
│
├── CbdcBridgeResolver (MINT / REDEEM / TRANSFER / SWAP)
└── CbdcSandboxServer
├── ECB Sandbox (USDC proxy)
├── FED Sandbox (USDT proxy)
└── BIS mBridge (cross-network SWAP)
│
▼
Settlement Service ← decides: settle immediately, or hold and wait for confirmation
│
├── settled immediately → Ledger Service (DEBIT sender / CREDIT receiver)
│
└── pending → Ledger Service (DEBIT sender / CREDIT SETTLEMENT_CLEARING)
│
(admin confirms later)
│
├── success → Ledger Service (DEBIT SETTLEMENT_CLEARING / CREDIT receiver)
└── failure → Ledger Service (DEBIT SETTLEMENT_CLEARING / CREDIT sender — hold released)
│
▼
Reconciliation Service ← compares COMPLETED payments against rail.checkStatus()
Register → Authenticate → Create Wallet → Deposit Funds
↓
Initiate Payment
↓
KYC Check (transaction limit by level)
↓
Balance Check
↓
FX Conversion (if receiveCurrency differs)
↓
AML Check (daily limit + structuring detection)
↓
RailRouter → MockRail | StripeRail | CbdcRail
↓
Settlement Service decides:
│
├── settledImmediately = true → Ledger posts DEBIT/CREDIT now → payment COMPLETED
│
└── settledImmediately = false → Ledger posts a HOLD (DEBIT sender / CREDIT clearing)
→ payment AWAITING_SETTLEMENT
↓
(admin-triggered) confirm settlement → rail.checkStatus()
├── SUCCESS → Ledger posts completion (DEBIT clearing / CREDIT receiver) → COMPLETED
└── FAILED → Ledger posts release (DEBIT clearing / CREDIT sender) → FAILED
↓
(later, admin-triggered) Reconciliation → rail.checkStatus() → MATCHED / MISMATCH
Application runs on http://localhost:8080/api.
Swagger UI: http://localhost:8080/api/swagger-ui.html
A default admin user is seeded via V1__initial_schema.sql for admin-only endpoints (KYC upgrade, AML flagged list,
reconciliation, settlement confirmation):
email: admin@paymentbridge.local
password: Admin123!
Sandbox credentials only — replace or remove the seed before any real deployment.
Register
curl -X POST http://localhost:8080/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "Password123!"}'Save token and userId from the response.
Login
curl -X POST http://localhost:8080/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "Password123!"}'Create Wallet
curl -X POST "http://localhost:8080/api/v1/wallets?currency=USD" \
-H "Authorization: Bearer <token>"Supported currencies: USD, EUR, GBP, AED, TRY, USDC, USDT
Deposit Funds (sandbox only)
curl -X POST http://localhost:8080/api/v1/wallets/deposit \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"amount": 1000.00, "currency": "USD"}'Get My Wallets
curl -X GET http://localhost:8080/api/v1/wallets \
-H "Authorization: Bearer <token>"Get My KYC Level
curl -X GET http://localhost:8080/api/v1/compliance/kyc/me \
-H "Authorization: Bearer <token>"Upgrade KYC Level (admin only)
curl -X PUT http://localhost:8080/api/v1/compliance/kyc/{userId} \
-H "Authorization: Bearer <admin-token>" \
-H "Content-Type: application/json" \
-d '{"kycLevel": "BASIC", "notes": "verified manually"}'Get My AML Flags
curl -X GET http://localhost:8080/api/v1/compliance/aml/me \
-H "Authorization: Bearer <token>"Get All Flagged Transactions (admin only)
curl -X GET http://localhost:8080/api/v1/compliance/aml/flagged \
-H "Authorization: Bearer <admin-token>"All requests require an X-Idempotency-Key header.
Standard Payment
curl -X POST http://localhost:8080/api/v1/payments \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-H "X-Idempotency-Key: pay-001" \
-d '{
"receiverWalletAccountCode": "WALLET-{receiverUserId}-USD",
"amount": 100.00,
"currency": "USD",
"railType": "MOCK",
"description": "test payment"
}'Payment with FX Conversion
curl -X POST http://localhost:8080/api/v1/payments \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-H "X-Idempotency-Key: pay-002" \
-d '{
"receiverWalletAccountCode": "WALLET-{receiverUserId}-EUR",
"amount": 100.00,
"currency": "USD",
"receiveCurrency": "EUR",
"railType": "MOCK",
"description": "fx payment"
}'Response includes receiveAmount, receiveCurrency, fxRate.
Stripe Sandbox (requires VPN + STRIPE_API_KEY)
curl -X POST http://localhost:8080/api/v1/payments \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-H "X-Idempotency-Key: pay-stripe-001" \
-d '{
"receiverWalletAccountCode": "WALLET-{receiverUserId}-USD",
"amount": 50.00,
"currency": "USD",
"railType": "STRIPE",
"description": "stripe payment"
}'Not part of the automated test script since it depends on external network access.
CBDC Transfer (USDC → USDC, settles immediately)
curl -X POST http://localhost:8080/api/v1/payments \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-H "X-Idempotency-Key: cbdc-001" \
-d '{
"receiverWalletAccountCode": "WALLET-{receiverUserId}-USDC",
"amount": 10.00,
"currency": "USDC",
"railType": "CBDC_SANDBOX",
"description": "cbdc transfer"
}'TRANSFER → ECB_SANDBOX → STLD (immediate) → settledImmediately = true → payment COMPLETED right away.
CBDC Mint (USD → USDC, goes through Settlement)
curl -X POST http://localhost:8080/api/v1/payments \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-H "X-Idempotency-Key: cbdc-mint-001" \
-d '{
"receiverWalletAccountCode": "WALLET-{receiverUserId}-USDC",
"amount": 50.00,
"currency": "USD",
"receiveCurrency": "USDC",
"railType": "CBDC_SANDBOX",
"description": "mint usdc"
}'MINT → ECB_SANDBOX → ACCP (not STLD) → settledImmediately = false → payment AWAITING_SETTLEMENT, funds
held in SETTLEMENT_CLEARING until an admin confirms.
CBDC Swap (USDC → USDT, cross-network, goes through Settlement)
curl -X POST http://localhost:8080/api/v1/payments \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-H "X-Idempotency-Key: cbdc-swap-001" \
-d '{
"receiverWalletAccountCode": "WALLET-{receiverUserId}-USDT",
"amount": 10.00,
"currency": "USDC",
"receiveCurrency": "USDT",
"railType": "CBDC_SANDBOX",
"description": "cross-network swap"
}'SWAP → BIS_MBRIDGE → ACCP → AWAITING_SETTLEMENT, same hold/confirm flow as MINT.
Simulate Failed Payment
curl -X POST http://localhost:8080/api/v1/payments \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-H "X-Idempotency-Key: pay-fail-001" \
-d '{
"receiverWalletAccountCode": "WALLET-{receiverUserId}-USD",
"amount": 50.00,
"currency": "USD",
"railType": "MOCK",
"description": "fail this payment"
}'Duplicate Idempotency Key (reuse a key already used above → 409 CONFLICT)
curl -X POST http://localhost:8080/api/v1/payments \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-H "X-Idempotency-Key: pay-001" \
-d '{
"receiverWalletAccountCode": "WALLET-{receiverUserId}-USD",
"amount": 100.00,
"currency": "USD",
"railType": "MOCK"
}'Get Payment by ID
curl -X GET http://localhost:8080/api/v1/payments/{paymentId} \
-H "Authorization: Bearer <token>"Get My Payments
curl -X GET http://localhost:8080/api/v1/payments \
-H "Authorization: Bearer <token>"Get Ledger for a Payment
curl -X GET http://localhost:8080/api/v1/ledger/payments/{paymentId} \
-H "Authorization: Bearer <token>"Returns two entries for an immediately-settled payment (DEBIT + CREDIT), or two entries for a held payment that
hasn't confirmed yet (DEBIT sender + CREDIT SYS-SETTLEMENT-CLEARING) — with two more appearing once an admin
confirms it.
Get Account History
curl -X GET "http://localhost:8080/api/v1/ledger/accounts/WALLET-{userId}-USD" \
-H "Authorization: Bearer <token>"The same endpoint also works for system accounts, e.g. SYS-SETTLEMENT-CLEARING, SYS-FEE, SYS-SUSPENSE.
List Available Networks
curl -X GET http://localhost:8080/api/cbdc-sandbox/networksQuery Transaction Status
curl -X GET http://localhost:8080/api/cbdc-sandbox/status/{txId}Compares every COMPLETED payment against the real status reported by its rail. Payments still AWAITING_SETTLEMENT
are correctly excluded — they haven't reached finality yet, so there's nothing to reconcile.
Trigger a Reconciliation Run
curl -X POST http://localhost:8080/api/v1/reconciliation/runs \
-H "Authorization: Bearer <admin-token>"List All Runs
curl -X GET http://localhost:8080/api/v1/reconciliation/runs \
-H "Authorization: Bearer <admin-token>"Get a Specific Run
curl -X GET http://localhost:8080/api/v1/reconciliation/runs/{runId} \
-H "Authorization: Bearer <admin-token>"Get Results (optionally mismatches only)
curl -X GET "http://localhost:8080/api/v1/reconciliation/runs/{runId}/results?mismatchesOnly=true" \
-H "Authorization: Bearer <admin-token>"Handles payments that a rail accepted but hasn't finalized yet (AWAITING_SETTLEMENT). Every hold, completion, and
release is a balanced double-entry pair against the internal SETTLEMENT_CLEARING account — never a single
unpaired entry.
List Pending Settlements
curl -X GET http://localhost:8080/api/v1/settlements/pending \
-H "Authorization: Bearer <admin-token>"Get a Settlement Record by ID
curl -X GET http://localhost:8080/api/v1/settlements/{settlementId} \
-H "Authorization: Bearer <admin-token>"Get the Settlement Record for a Payment
curl -X GET http://localhost:8080/api/v1/settlements/payment/{paymentId} \
-H "Authorization: Bearer <admin-token>"Confirm a Pending Settlement
curl -X POST http://localhost:8080/api/v1/settlements/{settlementId}/confirm \
-H "Authorization: Bearer <admin-token>"Re-checks the rail via checkStatus(). If the rail now reports success, the hold is released to the receiver and
the payment becomes COMPLETED. If the rail reports failure, the hold is released back to the sender, the payment
becomes FAILED, and an AML flag is raised automatically.
chmod +x test.sh
./test.shThe script exercises every endpoint documented above — except the Stripe payment, which needs external network access — across the full lifecycle: register sender and receiver, admin login, wallet creation, KYC check and admin upgrade, deposits, a standard payment, an FX payment, get-payment-by-id, get-my-payments, balance check, ledger and account-history lookups, a simulated failure, idempotency rejection, AML flags (self and admin-wide), a CBDC transfer (immediate settlement), a CBDC mint and a CBDC swap (both going through the settlement hold/confirm flow), CBDC network listing and transaction-status lookup, the full reconciliation cycle, and the full settlement cycle (list pending, get by id, get by payment id, confirm).
app:
fx:
provider: mock # default — no API key needed
# provider: exchangeratesapi # real rates from exchangeratesapi.io
api-key: your_key_hereMock rates (USD base):
| Currency | Rate |
|---|---|
| EUR | 0.91 |
| GBP | 0.79 |
| AED | 3.67 |
| TRY | 32.50 |
| USDT | 1.00 |
| USDC | 1.00 |
| Level | Max Transaction | Daily Limit |
|---|---|---|
| UNVERIFIED | 100 USD | 500 USD |
| BASIC | 5,000 USD | 20,000 USD |
| FULL | 1,000,000 USD | 1,000,000 USD |
| Rule | Description |
|---|---|
| Daily Limit | Total daily payments cannot exceed KYC daily limit |
| Structuring | Round amounts ≥ 5,000 are flagged for review |
| Reconciliation Mismatch | A STATUS_MISMATCH found during reconciliation is auto-flagged |
| Settlement Rejection | A settlement rejected by the rail on confirm is auto-flagged |
| From | To | Operation | Network | Status | Settles Via |
|---|---|---|---|---|---|
| USD/EUR | USDC | MINT | ECB Sandbox | ACCP | Settlement hold + confirm |
| USD/EUR | USDT | MINT | FED Sandbox | ACCP | Settlement hold + confirm |
| USDC | USD/EUR | REDEEM | ECB Sandbox | ACCP | Settlement hold + confirm |
| USDT | USD/EUR | REDEEM | FED Sandbox | ACCP | Settlement hold + confirm |
| USDC | USDC | TRANSFER | ECB Sandbox | STLD (immediate) | Immediate |
| USDT | USDT | TRANSFER | FED Sandbox | STLD (immediate) | Immediate |
| USDC | USDT | SWAP | BIS mBridge | ACCP | Settlement hold + confirm |
| USDT | USDC | SWAP | BIS mBridge | ACCP | Settlement hold + confirm |
| Code | Meaning |
|---|---|
| PDNG | Pending network confirmation |
| ACCP | Accepted by network |
| STLD | Settled — final, irreversible |
| RJCT | Rejected (with reason code) |
| Type | Meaning |
|---|---|
| MATCHED | Local and rail status/amount agree |
| STATUS_MISMATCH | Status differs between local and rail — auto-flagged for AML |
| AMOUNT_MISMATCH | Status matches but amount differs |
| ORPHANED_LOCAL | Rail does not recognize the transaction |
| ORPHANED_REMOTE | Rail has a transaction we never recorded (documented, not triggerable in sandbox) |
| UNVERIFIABLE | Rail was unreachable or returned no data |
| Status | Meaning |
|---|---|
| PENDING | Rail accepted the payment (ACCP) but hasn't settled it (STLD) yet |
| SETTLED | Admin confirmed and the rail reports success — funds released to receiver |
| FAILED | Admin confirmed and the rail reports failure — hold released back to sender |
| Account | Purpose |
|---|---|
SYS-FEE |
Fee collection |
SYS-SUSPENSE |
General suspense account |
SYS-SETTLEMENT-CLEARING |
Temporary holding account while a payment awaits settlement confirmation |
payment-bridge-core
├── config
├── security
├── common
│ └── enums (Currency, KycLevel, AmlStatus, CbdcNetwork, CbdcTxStatus, CbdcOperationType,
│ ReconciliationResultType, SettlementStatus, SystemLedgerAccount, UserRole)
├── exception
├── user
├── payment
├── wallet
├── ledger
├── compliance
│ ├── entity (KycProfile, AmlFlag)
│ ├── service (KycService, AmlService)
│ └── controller (ComplianceController)
├── rails
│ ├── MockRail
│ ├── stripe (StripeRail)
│ └── cbdc
│ ├── CbdcRail
│ ├── CbdcBridgeResolver
│ ├── CbdcProperties
│ ├── dto (CbdcSettlementRequest, CbdcSettlementResponse)
│ └── sandbox (CbdcSandboxServer)
├── fx
│ ├── provider (MockFxRateProvider, ExchangeRatesApiFxProvider)
│ └── service (FxService)
├── reconciliation
│ ├── entity (ReconciliationRun, ReconciliationResult)
│ ├── dto (ReconciliationRunResponse, ReconciliationResultResponse)
│ ├── repository
│ ├── service (ReconciliationService)
│ └── controller (ReconciliationController)
└── settlement
├── entity (SettlementRecord)
├── dto (SettlementRecordResponse)
├── repository (SettlementRecordRepository)
├── service (SettlementService)
└── controller (SettlementController)
| Feature | Status | Notes |
|---|---|---|
| Payment Core | ✅ | lifecycle, idempotency, status tracking |
| Auth & User | ✅ | JWT, register, login, role-based access |
| Wallet | ✅ | multi-currency, balance from ledger |
| FX Engine | ✅ | mock + real provider, pluggable |
| StripeRail | ✅ | Stripe sandbox integration |
| KYC / AML | ✅ | level-based limits, daily limit, flagging |
| CBDC Layer | ✅ | ISO 20022, 4 operations, 4 networks |
| Reconciliation | ✅ | rail/ledger comparison, 6 result types, admin-only |
| Settlement | ✅ | hold/confirm via a clearing account, admin-only, double-entry throughout |
- balance derived from ledger — never stored separately
- every payment rail behind a common interface
- every financial event recorded in the ledger
- compliance checks before every payment
- reconciliation as a first-class citizen, not an afterthought
- rail acceptance and payment finality are two different things
- ledger entries are immutable — corrections are new entries, never edits
- complexity added only when justified
This project is for educational and architectural exploration only.
Not designed for production use, regulatory compliance, or real-money handling.