Decentralized parking management powered by blockchain, the x402 payment protocol (EVM + XRPL rails), and multi-currency Stripe checkout.
Most parking lots today are fully automated: they scan license plates at entry and exit and charge the driver's credit card at the gate. Many lots also offer integrated apps to streamline payment. Yet the experience remains fragmented and frustrating. Some lots issue physical cards with barcodes or QR codes; others don't. To pay via app, drivers must scan a QR code — which often fails during peak hours. Some lots still require scanning the card at exit even after the driver has already paid through the app. Parker addresses these broken flows by moving parking onto the blockchain: tickets become NFTs, payments run on-chain via x402, and verification is instant — no cards, no QR scans, no gate confusion.
Country operator runs one deployment; lots are per-operator configs.
Parker replaces fragmented centralized parking app flows (e.g., municipal/operator apps such as Pango, EasyPark, or RingGo) with a trustless, blockchain-based system. Parking tickets are NFTs, payments happen via x402 (Base or XRPL) or Stripe (credit card) — each lot configures its own local currency and accepted payment methods. Verification is instant — no more "communication errors" at the gate.
For the architectural rationale, see docs/WHY_BLOCKCHAIN.md.
For operational and fallback scenarios, see docs/use-cases.md (including §21 Policy & Settlement Enforcement).
For policy lifecycle (Grant → Decision → Enforcement), unit conventions, and env vars, see docs/policy-lifecycle.md.
Parker is multi-country and currency-agnostic at the platform level. In practice, each operator scopes its own deployment to a target market (single country or region) and configures lots within that deployment.
The DEPLOYMENT_COUNTRIES environment variable controls:
- Driver registration — only countries in the deployment are shown; single-country deployments auto-select and hide the picker
- ALPR — plate format validation is restricted to the deployment's country patterns
- Currency & payments — each lot configures its own currency, but all lots in a deployment typically share the same local currency and FX rate
# Single-country deployment (Israel)
DEPLOYMENT_COUNTRIES=IL
# Regional deployment (EU)
DEPLOYMENT_COUNTRIES=DE,FR,ES,IT,NL,GB,AT,BE
┌───────────────────┐
│ Hedera (NFTs) │
│ HTS Token Svc │
└────────▲──────────┘
│
┌─────────────┐ ┌────────┴─────────┐ ┌────────────┐
│ Driver App │◄───────►│ Parker API │◄───────►│ Gate App │
│ (Wallet) │ │ (Express.js) │ │ (Lot Ops) │
└──────┬──────┘ └───────┬──────────┘ └────────────┘
│ │
│ ▼
│ ┌────────────────────┐
│ │ Policy Layer │
│ │ (caps/allowlists) │
│ └─────────┬──────────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ x402 rail(s) │ │ Stripe │
│ Base / XRPL │ │ (Card/local) │
└──────────────┘ └──────────────┘
Hedera-first + dual-rail architecture (with optional Base components):
- Hedera — Parking session NFTs via native Token Service (mint on entry, burn on exit)
- x402 crypto rail — settlement can run on Base (EVM) or XRPL (selected via
X402_NETWORK) - Base Sepolia (optional per deployment) — DriverRegistry sync and optional EVM x402 settlement
- Stripe — Credit card payments in the lot's local currency (any Stripe-supported currency)
- Car arrives at gate
- Gate camera captures plate image, ALPR extracts plate number via Google Vision API
- API checks if plate belongs to a registered driver
- Parking NFT minted on Hedera via HTS (AES-256-GCM encrypted metadata carrying
plateHash, lot ID, entry time) — write-ahead: the on-chain NFT is the authoritative proof of entry - Session created in DB, WebSocket notifies driver app in real-time
- Gate opens
- Car approaches exit, plate scanned again
- System finds active parking session and calculates fee in the lot's local currency
- API returns payment options based on lot config and policy evaluation:
- Policy decision — evaluates caps (per-tx/session/day in stablecoin minor units), lot/geo/rail/asset allowlists (XRP, IOUs e.g. USDC/RLUSD, EVM ERC20), and risk signals; may require explicit approval. Decision is bound to the payment intent (
decisionId).- Allowlist semantics are strict:
undefined= no restriction,[]= deny-all,[values]= restrict to listed values.
- Allowlist semantics are strict:
- x402 (crypto) — fee converted via FX to stablecoin; payment proof verified per selected network (EVM tx hash or XRPL tx hash). On XRPL, after verification the settlement is enforced against the decision (amount ≤ per-tx cap, same rail/asset); if enforcement fails, exit returns 403 and session is not closed. XRPL UX is Xaman-first with manual tx-hash fallback.
- Stripe (card) — Stripe Checkout session created in the lot's currency; driver redirected to Stripe-hosted page; webhook confirms payment
- Policy decision — evaluates caps (per-tx/session/day in stablecoin minor units), lot/geo/rail/asset allowlists (XRP, IOUs e.g. USDC/RLUSD, EVM ERC20), and risk signals; may require explicit approval. Decision is bound to the payment intent (
- On payment confirmation (either rail): parking NFT burned on Hedera, session closed in DB
- Gate opens automatically, driver app notified via WebSocket
Following the Hedera pragmatic design patterns and the same hybrid model used by MINGO Tickets, Parker uses a layered resilience strategy:
| Layer | Source | When |
|---|---|---|
| 1. PostgreSQL | Fast path for all operations | Default — DB is up |
| 2. Hedera Mirror Node | Read-only fallback — verifies NFT exists + reads entry metadata | DB unreachable |
| 3. Gate-side cache | Local session cache built from WebSocket events | Both DB and Mirror Node down |
Why not make Hedera the primary? Blockchain consensus is fast (~3-5s on Hedera) but PostgreSQL is faster (~1ms). At a busy parking gate, every second matters. The DB handles the operational speed; Hedera provides the trust guarantee and fallback. This matches how MINGO uses Hedera as an "invisible trust layer" rather than the operational database.
Key design principle: The Hedera NFT is minted before the DB write on entry (write-ahead). This means the on-chain record is always the leading indicator — if the DB loses a record, the NFT on Hedera proves the car is parked. See SPEC.md §11 for the full resilience architecture.
The app has three main components:
- Register with license plate, country, car make/model — off-chain via API, with optional on-chain sync to
DriverRegistryon Base - Coinbase Smart Wallet integration (passkey-based, no seed phrase)
- Live dashboard with active parking session card (lot name, address with Google Maps link, real-time duration timer + estimated cost)
- Full parking history with date, lot, duration, fee, NFT token ID
- Profile page with vehicle details and wallet address
- Real-time WebSocket updates when sessions start/end
- Camera feed with ALPR overlay — captures frame, sends to scan API, gets plate back
- Lot name and address displayed in header (fetched from lot status API)
- Entry/exit mode toggle with manual plate input fallback
- Live gate status indicator (open/closed) with operation result feedback
- Session manager — searchable table of active sessions with live duration and estimated fees
- Operator dashboard — lot occupancy, active session count, average duration
- Lot settings page — configure pricing (rate/hr, billing increment, daily cap), capacity, address
- WebSocket connection with live status indicator
- Offline-capable: local session cache built from WebSocket events — if the API is unreachable, the gate can still validate exits from its cache and open the gate (payment deferred)
- Express.js with PostgreSQL for off-chain indexing
- ALPR pipeline via
@parker/alpr(Google Cloud Vision) - Hedera integration via
@parker/hedera(@hashgraph/sdk) — mints parking NFTs on entry, burns on exit - Optional Base integration via viem — DriverRegistry reads/sync on Base Sepolia
- Multi-currency payment — each lot defines its own currency (USD, EUR, GBP, etc.) and accepted payment methods
- Policy layer — entry grant (lot/geo/rail/asset allowlists); payment decision at exit (caps in stablecoin minor, rail/asset); settlement enforcement on XRPL (re-check decision vs verified transfer); policy events stored for audit (
policy_events,policy_grants, decision payload bydecisionId) - x402 payment middleware — returns HTTP 402 with crypto amount (FX-converted from local currency) after policy evaluation; verifies payment proof on retry via network-aware adapters (EVM + XRPL)
- Stripe Checkout — creates payment sessions in the lot's local currency; webhook-driven session closure
- Pricing service — currency-agnostic FX conversion via configurable rates (
FX_RATE_{FROM}_{TO}env vars) - WebSocket server for real-time gate and driver events
- Full CRUD for drivers, sessions, and lots
| Layer | Technology |
|---|---|
| Monorepo | pnpm workspaces + Turborepo |
| Frontend | Next.js 14 (PWA, mobile-first, Tailwind CSS) |
| Driver Wallet | Coinbase Smart Wallet via wagmi |
| Payments (crypto) | x402 protocol (Base EVM and XRPL settlement rails) |
| Payments (card) | Stripe Checkout (any Stripe-supported currency) |
| Parking NFTs | Hedera Token Service (native HTS, @hashgraph/sdk) |
| Driver Registry (optional) | Solidity 0.8.20 + Hardhat (Base Sepolia) |
| ALPR | Google Cloud Vision API |
| Backend | Node.js + Express + TypeScript |
| Database | PostgreSQL 16 (Docker) |
| Real-time | WebSocket (ws) |
| Blockchain Clients | @hashgraph/sdk (Hedera) + viem (Base) |
- Node.js 20+ (see
.nvmrc) - pnpm 9+
- Docker (for PostgreSQL)
# Install dependencies
pnpm install
# Start PostgreSQL (creates schema + seeds demo data)
docker compose -f infra/docker-compose.yml up -d
# Copy environment files
cp apps/api/.env.example apps/api/.env
cp apps/driver/.env.example apps/driver/.env
cp apps/gate/.env.example apps/gate/.env
# Set deployment country (controls ALPR plate format + driver registration)
# Edit each .env and set DEPLOYMENT_COUNTRIES to your target market, e.g.:
# DEPLOYMENT_COUNTRIES=IL (Israel)
# DEPLOYMENT_COUNTRIES=US (United States)
# DEPLOYMENT_COUNTRIES=DE,FR,ES (EU region)
# Start all apps in dev mode
pnpm devThis starts:
- API on
http://localhost:3001 - Driver app on
http://localhost:3000 - Gate app on
http://localhost:3002
For local development, .env files are convenient. For production, inject secrets at runtime from a managed secret store.
- HashiCorp Vault (example):
export HEDERA_PRIVATE_KEY="$(vault kv get -field=HEDERA_PRIVATE_KEY secret/parker/prod)" export NFT_ENCRYPTION_KEY="$(vault kv get -field=NFT_ENCRYPTION_KEY secret/parker/prod)" pnpm --filter api start
- AWS Secrets Manager (example):
export HEDERA_PRIVATE_KEY="$(aws secretsmanager get-secret-value --secret-id parker/prod --query 'SecretString' --output text | jq -r '.HEDERA_PRIVATE_KEY')" export NFT_ENCRYPTION_KEY="$(aws secretsmanager get-secret-value --secret-id parker/prod --query 'SecretString' --output text | jq -r '.NFT_ENCRYPTION_KEY')" pnpm --filter api start
- Doppler (example):
doppler run -- pnpm --filter api start
Never store production private keys on developer laptops, in git, or in plaintext .env files. Rotate keys after any suspected exposure.
The database is auto-seeded with two demo lots and a test driver. Each lot has its own currency, rates, and payment methods — see apps/api/src/db/seed.sql for details.
The system is fully currency-agnostic — each lot configures its own ISO 4217 currency and rates. Seed data uses sample values for demonstration.
Plates are stored in normalized form (alphanumeric, no dashes). The API normalizes all incoming plates automatically, so
12-345-67,12 345 67, and1234567all resolve to the same driver.
Use the default seeded values:
lotId=lot-01plateNumber=1234567
# Entry (starts session + mints Hedera HTS NFT when configured)
curl -X POST http://localhost:3001/api/gate/entry \
-H "Content-Type: application/json" \
-H "Idempotency-Key: entry-lot01-1234567-001" \
-d '{"plateNumber":"1234567","lotId":"lot-01"}'
# Exit (returns fee + payment options; session closes after payment confirmation)
curl -X POST http://localhost:3001/api/gate/exit \
-H "Content-Type: application/json" \
-H "Idempotency-Key: exit-lot01-1234567-001" \
-d '{"plateNumber":"1234567","lotId":"lot-01"}'Where to look:
- Gate UI:
http://localhost:3002(live entry/exit status + cache badge) - Driver UI:
http://localhost:3000(active session, payment flow, history) - Hedera NFT: in Driver history, click the Hashscan link (set
NEXT_PUBLIC_HEDERA_TOKEN_IDinapps/driver/.envto enable links)
# 1. Create a Hedera testnet account at https://portal.hedera.com/register
# 2. Add credentials to apps/api/.env:
# HEDERA_ACCOUNT_ID=0.0.xxxxx
# HEDERA_PRIVATE_KEY=302e...
# HEDERA_NETWORK=testnet
# 3. Create the NFT collection on Hedera:
pnpm --filter @parker/hedera setup
# 4. Copy the output HEDERA_TOKEN_ID into apps/api/.envParker supports two payment rails per lot, both optional:
Stripe (credit card) — charges in the lot's configured currency (USD, EUR, GBP, etc.):
# Add to apps/api/.env:
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_SUCCESS_URL=http://localhost:3000/payment/success
STRIPE_CANCEL_URL=http://localhost:3000/payment/cancelx402 (crypto rails) — converts the lot's local currency fee via FX and verifies settlement proof on the selected rail:
# Add to apps/api/.env:
X402_STABLECOIN=USDC
X402_NETWORK=base-sepolia
LOT_OPERATOR_WALLET=0x...
# For XRPL settlement (required when X402_NETWORK starts with "xrpl:")
XRPL_RPC_URL=wss://s.altnet.rippletest.net:51233
XAMAN_API_URL=https://xumm.app
XAMAN_API_KEY=...
XAMAN_API_SECRET=...
# Required for non-XRP XRPL assets (e.g., RLUSD IOU):
XRPL_ISSUER=r...
# FX rates (only needed when lot currency differs from stablecoin base):
FX_RATE_EUR_USD=1.08
FX_RATE_GBP_USD=1.27Supported examples:
X402_NETWORK=base-sepolia(EVM, ERC-20 transfer verification)X402_NETWORK=xrpl:testnet(XRPL, tx-hash verification via adapter)
Branding note: Xaman logo assets used in the UI are sourced from the official XRPL Labs branding repository:
https://github.com/XRPL-Labs/Xaman-Branding
Branding note: Coinbase wallet branding should be sourced from Coinbase official brand/press assets:
https://www.coinbase.com/press
Each lot's currency and paymentMethods are stored in the database and can be updated via PUT /api/gate/lot/:lotId.
# Compile contracts
pnpm contracts:compile
# Run tests
pnpm contracts:test
# Deploy DriverRegistry to Base Sepolia (requires PRIVATE_KEY in contracts/.env)
pnpm contracts:deployThe workspace uses Vitest as the single test runner for all Node packages and the API app (contracts use Hardhat). Do not mix runners (e.g. no node --test alongside Vitest in the same package).
Test layout:
- Packages (unit):
packages/<name>/test/*.test.ts - API (integration):
apps/api/test/*.test.ts(with subdirstest/routes/,test/services/,test/middleware/,test/policy/as needed)
Run tests per package: pnpm --filter <package-name> test, or from repo root pnpm --filter api test, etc.
parker-app/
├── apps/
│ ├── api/ # Express API server
│ │ ├── src/
│ │ │ ├── db/ # PostgreSQL schema, queries, seed data
│ │ │ ├── routes/ # drivers, gate, sessions, webhooks endpoints
│ │ │ ├── services/ # hedera.ts, blockchain.ts, stripe.ts, pricing.ts
│ │ │ ├── middleware/ # wallet auth
│ │ │ └── ws/ # WebSocket server
│ │ └── ...
│ ├── driver/ # Driver PWA (Next.js)
│ │ ├── src/
│ │ │ ├── app/ # pages: dashboard, register, history, profile, session detail
│ │ │ ├── components/ # SessionCard, WalletButton
│ │ │ ├── hooks/ # useDriverProfile, useParkerSocket
│ │ │ ├── lib/ # API client
│ │ │ └── providers/ # WalletProvider (wagmi + Coinbase Smart Wallet)
│ │ └── ...
│ └── gate/ # Gate operator app (Next.js)
│ ├── src/
│ │ ├── app/ # pages: live gate, dashboard, sessions, settings
│ │ ├── components/ # CameraFeed, PlateResult, GateStatus
│ │ ├── hooks/ # useGateSocket, useSessionCache (offline resilience)
│ │ └── lib/ # API client
│ └── ...
├── contracts/ # Solidity smart contracts
│ ├── contracts/ # DriverRegistry.sol (+ legacy ParkingNFT.sol reference contract)
│ ├── test/ # Full test suites for both contracts
│ └── scripts/ # Deploy script
├── packages/
│ ├── core/ # Shared types, utils (calculateFee, formatPlate, hashPlate), contract ABIs
│ ├── hedera/ # Hedera HTS integration (mint/burn NFTs, mirror node queries, setup script)
│ ├── alpr/ # License plate recognition (Google Vision + country-aware plate normalization)
│ ├── x402/ # x402 middleware/client + EVM verification primitives
│ ├── x402-xrpl-settlement-adapter/ # XRPL settlement verification adapter
│ ├── policy-core/ # Pure policy evaluation (entry grant, payment decision) + enforcePayment at settlement
│ ├── settlement-core/ # Rail-agnostic settlement types + XRPL helpers (verification in x402 adapter)
│ ├── observability/ # Structured logging + metrics primitives
│ ├── tsconfig/ # Shared TypeScript configs
│ └── eslint-config/ # Shared ESLint config
├── infra/ # Docker Compose (PostgreSQL)
├── SPEC.md # Detailed technical specification
└── turbo.json # Turborepo pipeline config
| Method | Path | Description |
|---|---|---|
| POST | /api/drivers/register |
Register new driver + vehicle |
| GET | /api/drivers/wallet/:address |
Look up driver by wallet address |
| GET | /api/drivers/:plate |
Get driver profile by plate |
| PUT | /api/drivers/:plate |
Update driver profile |
| DELETE | /api/drivers/:plate |
Deactivate driver |
| Method | Path | Description |
|---|---|---|
| GET | /api/sessions/active/:plate |
Get active parking session |
| GET | /api/sessions/history/:plate |
Get session history |
| GET | /api/sessions/:sessionId/timeline |
Get ordered lifecycle timeline events (x-gate-api-key required when configured) |
Timeline response shape:
{
"sessionId": "11111111-1111-4111-8111-111111111111",
"state": "payment_required",
"eventCount": 2,
"events": [
{
"eventType": "SESSION.CREATED",
"createdAt": "2026-03-08T12:00:00Z",
"metadata": {
"lotId": "lot_1"
}
}
]
}| Method | Path | Description |
|---|---|---|
| POST | /api/gate/entry |
Process vehicle entry (plate string or image). Requires Idempotency-Key header |
| POST | /api/gate/exit |
Process vehicle exit + return payment options (x402 + Stripe). Requires Idempotency-Key header |
| POST | /api/gate/xrpl/xaman-intent |
Create Xaman payload for pending XRPL payment (Xaman-first flow) |
| GET | /api/gate/xrpl/xaman-status/:payloadUuid |
Poll Xaman payload resolution and get XRPL tx hash |
| POST | /api/gate/scan |
ALPR: upload image, get plate number |
| GET | /api/gate/lot/:lotId/status |
Lot occupancy, config, currency, payment methods |
| GET | /api/gate/lot/:lotId/sessions |
Active sessions list |
| PUT | /api/gate/lot/:lotId |
Update lot settings (rates, currency, payment methods) |
| Method | Path | Description |
|---|---|---|
| POST | /api/webhooks/stripe |
Stripe payment confirmation (closes session + burns NFT) |
| Method | Path | Description |
|---|---|---|
| GET | /healthz |
Liveness probe (basic process health) |
| GET | /readyz |
Readiness probe (DB + Hedera + Mirror Node + payment rail config) |
| GET | /metrics |
In-memory metrics snapshot (mint/burn latency, mirror lag, failures) |
| Path | Description |
|---|---|
/ws/gate/:lotId |
Real-time gate events (entry/exit) |
/ws/driver/:plate |
Real-time session updates for driver |
On-chain privacy: Plate numbers are never stored in plaintext on Hedera. Parker hashes the plate (plateHash) and then stores NFT metadata as an AES-256-GCM encrypted binary payload on HTS. Public Mirror Node readers see ciphertext bytes, not readable plate/lot/time fields. The API can decrypt metadata with NFT_ENCRYPTION_KEY for fallback lookups, while plaintext plate data remains only in the access-controlled PostgreSQL database. See SPEC.md §12.1 for the full privacy model.
For a focused adversarial analysis (replay attacks, cloned tickets, plate spoofing, race conditions, gate offline behavior, and payment disputes), see THREAT_MODEL.md.
Observability highlights:
- Structured JSON logs with request context (
request_id,session_id,lot_id) - Core metrics for mint/burn latency, mirror lag, failed exits, and payment failures
- Tracing is intentionally deferred for now (TODO) and will be added in a later phase.
The API enforces the following invariants:
- Plate normalization — all plates are stripped of dashes/spaces before storage and lookup, so format differences never cause mismatches. ALPR plate detection is scoped to the deployment's configured countries (
DEPLOYMENT_COUNTRIES) for higher accuracy - Lot validation on entry — entry is rejected if the lot doesn't exist, if it's full (capacity check), or if the driver is unregistered
- Lot mismatch on exit — a car can only exit from the lot it entered; mismatched
lotIdreturns400 - One active session per plate — enforced at both application level and via a PostgreSQL partial unique index (
WHERE status = 'active') - Fee guardrails —
calculateFeehandles zero/negative duration (minimum 1 billing increment), zero rate (fee = 0), and division-by-zero on billing interval (defaults to 15 min). Fees are rounded to 6 decimal places and capped bymaxDailyFee - Multi-currency — each lot defines its own currency (ISO 4217); the pricing service converts to stablecoin via configurable FX rates for the x402 rail, while Stripe charges in the lot's native currency directly
- Payment-before-close — the exit route returns payment options without closing the session; the session is only closed after payment confirmation (
X-PAYMENTproof for x402, or Stripe webhook) - Policy-gated payments — payment options and settlement constraints come from a policy decision (caps in stablecoin minor, allowlists); on XRPL, settlement is re-checked against the decision before closing (enforcement); policy events are stored for audit
- Network-aware x402 verification —
X-PAYMENTproofs are verified according toX402_NETWORK(EVM receipt parsing or XRPL payment verification) - Idempotent webhooks — Stripe webhook handler checks if the session is still active before closing, preventing duplicate closures on retry
- Idempotent gate mutations —
POST /api/gate/entryandPOST /api/gate/exitrequire anIdempotency-Key; duplicate retries return the cached response without re-running side effects - Input validation — required fields are checked on all mutation endpoints; numeric lot settings reject
NaN; session historylimit/offsetare sanitized and capped - Duplicate registration — returns
409with a clear error message instead of a generic 500 - Status constraints —
sessions.statusis enforced viaCHECKconstraint (active,completed,cancelled)
🚧 MVP in active development
- On-chain architecture: Hedera HTS parking NFTs + optional Base DriverRegistry/x402 rail
- API server with full CRUD, ALPR, blockchain integration
- Driver app: registration, wallet connect, session view, history, profile
- Gate app: camera feed, ALPR scan, dashboard, sessions, settings
- x402 payment flow (middleware + client)
- x402 XRPL settlement adapter (tx-hash verification path)
- Multi-currency support: per-lot currency config, FX conversion for x402 rail
- Stripe Checkout integration: credit card payments in any local currency
- Dual payment rails: Stripe (card) + x402 (Base/XRPL) per lot config
- Stripe webhook with idempotent session closure
- Real-time WebSocket events (gate auto-opens on payment from any rail)
- Database schema + seed data
- Input validation, fee guardrails, race-condition guards
- End-to-end smoke testing
- Deploy DriverRegistry to Base Sepolia (if Base registry sync is enabled)
- Create Hedera NFT collection on testnet
- Write-ahead NFT minting (mint before DB write on entry)
- Mirror Node fallback for exit when DB is unreachable
- Gate-side session cache for offline-capable exit validation
- On-chain payment verification (EVM via viem + XRPL via settlement adapter)
- XRPL payment verification via x402 settlement adapter (with manual tx-hash UX fallback)
- Policy layer: policy schema + decision logging (policy_events, policy_grants, decision payload by decisionId)
- Policy layer: per-tx/per-session/per-day spend caps (stablecoin minor) + lot/geo/rail/asset allowlists
- Policy layer: rail/asset selection from settlement-verifiable assets (XRPL XRP/IOU, EVM ERC20)
- Policy layer: bind decision to paymentId and enforce at settlement verification (XRPL path)
- Live FX rate feed (CoinGecko / Circle API) to replace static env var rates
- Wallet authentication (SIWE / EIP-4361)
- Push notifications
- Physical gate hardware integration (Phase 2)
MIT