Cross-border payment orchestration API. Multi-currency processing, real-time FX conversion, OFAC/EU sanctions screening, IBAN/SWIFT validation, and full payment lifecycle management through a strict state machine.
┌──────────────────────────────────────┐
│ CrossPay Engine │
│ │
Client ──> Fastify API ─┤ ┌─────────────────────────────────┐ │
(rate limit, │ │ Payment Engine │ │
auth, CORS) │ │ │ │
│ │ INITIATED ──> VALIDATED ──> ... │ │
│ │ State Machine │ │
│ └────┬──────┬──────┬──────────────┘ │
│ │ │ │ │
│ ┌────▼──┐ ┌─▼────┐ ┌▼───────────┐ │
│ │ FX │ │Sanc- │ │ IBAN/SWIFT │ │
│ │Service│ │tions │ │ Validator │ │
│ └───┬───┘ └──────┘ └─────────────┘ │
│ │ │
│ ┌───▼───────────────────────┐ │
│ │ PostgreSQL + Redis │ │
│ │ (payments, audit, cache) │ │
│ └───────────────────────────┘ │
└──────────────────────────────────────┘
Every payment moves through a strict state machine. Invalid transitions are rejected.
INITIATED ──> VALIDATED ──> SCREENED ──> QUOTED ──> CONFIRMED ──> PROCESSING ──> SETTLED ──> COMPLETED
│ │ │
▼ ▼ ▼
REJECTED HELD EXPIRED
│
▼
REJECTED or SCREENED (after review)
Additional states: CANCELLED (user-initiated), FAILED (processing error, retryable), REFUNDED (post-settlement reversal).
| Component | Technology | Why |
|---|---|---|
| Runtime | Node.js 20 | Non-blocking I/O for concurrent payment processing |
| Framework | Fastify 5 | 2x throughput vs Express, schema validation, plugin architecture |
| Database | PostgreSQL 16 | JSONB for flexible party data, ACID for financial transactions |
| Cache | Redis 7 | FX rate caching, idempotency key dedup |
| Validation | Zod | Runtime type checking, schema inference, better errors than Joi |
| Logging | Pino | Structured JSON, PII redaction, 5x faster than Winston |
| Security | Helmet, CORS, rate-limit | Defense in depth |
| Containers | Docker + Compose | Reproducible local dev, production parity |
| CI/CD | GitHub Actions | Automated test + security audit + deploy |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/payments | Create payment |
| GET | /api/v1/payments/:id | Get payment details |
| POST | /api/v1/payments/:id/validate | Validate IBAN/SWIFT/amounts |
| POST | /api/v1/payments/:id/screen | Run sanctions screening |
| POST | /api/v1/payments/:id/quote | Get FX quote (30s TTL) |
| POST | /api/v1/payments/:id/confirm | Confirm at quoted rate |
| POST | /api/v1/payments/:id/process | Submit to banking network |
| POST | /api/v1/payments/:id/cancel | Cancel (from eligible states) |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/fx/:from/:to | Live FX rate lookup |
| GET | /api/v1/validate/iban/:iban | IBAN validation with checksum |
| GET | /api/v1/validate/swift/:bic | SWIFT/BIC format validation |
| Method | Endpoint | Description |
|---|---|---|
| GET | /health | Liveness probe |
| GET | /ready | Readiness probe (checks DB) |
| GET | /metrics | System metrics |
curl -X POST http://localhost:3000/api/v1/payments \
-H "Content-Type: application/json" \
-d '{
"amount": 10000.00,
"sendCurrency": "USD",
"receiveCurrency": "EUR",
"sender": {
"name": "Acme Corp",
"iban": "GB29 NWBK 6016 1331 9268 19",
"swift": "NWBKGB2L",
"country": "GB"
},
"beneficiary": {
"name": "Schmidt GmbH",
"iban": "DE89 3704 0044 0532 0130 00",
"swift": "DEUTDEFF",
"country": "DE"
},
"purpose": "Invoice #INV-2026-0142",
"idempotencyKey": "unique-client-key-123"
}'Full ISO 7064 Mod 97-10 checksum verification with country-specific length enforcement.
Supported: 60+ countries (AL, AD, AT, AZ, BH, BY, BE, BA, BR, BG, ...)
Extracts: Country code, check digits, BBAN, bank identifier
Screens sender and beneficiary against OFAC SDN and EU consolidated lists.
Match types:
EXACT (confidence 1.0) - Direct name match
FUZZY (confidence 0.80+) - Levenshtein distance
PARTIAL (confidence 0.65) - Word-level overlap
Features:
- Unicode normalization (accents, diacritics)
- Alias matching
- Case insensitive
- Country pre-filtering
- Fastify Helmet (CSP, HSTS, X-Frame-Options)
- Rate limiting (100 req / 15 min per IP)
- API key authentication (SHA-256, timing-safe comparison)
- Zod input validation on all endpoints
- Parameterized SQL queries (injection prevention)
- Structured logging with PII field redaction
- Non-root Docker container
- Health checks with dependency verification
6 versioned migrations:
payments- Core payment table with state enum, JSONB party data, partial indexespayment_audit_log- Every state transition logged with actor and metadatafx_rates- Cached exchange rates with expiryidempotency_keys- Request deduplication with TTLwebhook_deliveries- Outbound webhook tracking with retry queuecleanup_procedures- DB functions for TTL expiry and key cleanup
npm test # Unit + integration tests
npm run test:adversarial # Security/adversarial testsThe adversarial suite documents every confirmed vulnerability with severity, risk description, and proposed fix. See tests/adversarial.test.js for the full report.
# Local development with Docker
cd docker && docker compose up -d
# Or manual setup
cp .env.example .env # Edit credentials
npm install
npm run db:migrate
npm startDocumented in tests/adversarial.test.js:
- No row-level locking on state transitions (double-spend risk in concurrent scenarios)
- Sanctions screening uses demo list (production requires OFAC SDN feed integration)
- No SSRF protection on webhook URLs
- BBAN structure not validated per-country
- Math.round instead of banker's rounding for FX conversions
- No phonetic matching (Soundex/Metaphone) for sanctions name transliterations
MIT
Henry Wyndham | henry@ubava.ee