Interactive demo showcasing modern A2A payments with real-time identity verification, instant account connectivity, and payment processing visibility.
Built with Straddle's unified fintech platform to demonstrate how cryptographically-linked paykeys deliver instant rail speed with card-level reliability.
Experience the future of ACH payments through a retro-styled split-screen interface:
- Left Panel: CLI-style terminal with interactive commands and real-time API request logs
- Right Panel: Live dashboard showing customer verification, bank linking, and payment status
- Real Integration: All API calls use Straddle's sandbox environment (no mocking, deterministic outcomes)
- Instant Updates: Server-Sent Events (SSE) provide real-time status changes
Traditional ACH runs on 1970s infrastructure—systematic delays, zero visibility, too much fraud. New instant rails lack the controls needed to scale safely.
Cryptographically-linked tokens ("paykeys") that marry identity to open banking, delivering instant rail speed with card-level reliability.
- Node.js ≥ 18.0.0
- npm ≥ 9.0.0
- Straddle sandbox API key (sign up here)
1. Clone and install
git clone https://github.com/straddleio/nerdcon-demo.git
cd nerdcon-demo
npm install2. Configure your API key
cd server
cp .env.example .envEdit server/.env and add your Straddle API key:
# Required
STRADDLE_API_KEY=sk_sandbox_your_key_here
STRADDLE_ENV=sandbox
# Optional - defaults shown
PORT=3001
CORS_ORIGIN=http://localhost:5173
# Webhook configuration (optional for local dev)
WEBHOOK_SECRET=your_webhook_secret_here
NGROK_URL=https://your-ngrok-id.ngrok.io
# Feature toggles (safe defaults for open source)
ENABLE_UNMASK=false
ENABLE_LOG_STREAM=false3. Start the demo
cd ..
npm run devThis starts two services concurrently:
- Express backend (port 3001)
- React frontend (port 5173)
Open http://localhost:5173 in your browser.
Type these commands in the browser terminal:
| Command | Description |
|---|---|
/demo |
Run complete flow (customer → bank → payment) |
/customer-create |
Create and verify a customer identity |
/customer-KYC |
Create customer with full KYC validation |
/create-paykey |
Link a bank account (supports Plaid or manual entry) |
/paykey-review |
Show review details for current paykey |
/paykey-decision approve|reject |
Approve or reject paykey in review |
/create-charge |
Process a payment |
/outcomes |
Show all available sandbox outcome values |
/info |
Show current demo state (IDs, status) |
/reset |
Clear all data and start fresh |
/help |
Show all available commands |
> /demo
Creating verified customer...
✓ Customer created: Alberta Bobbeth Charleson
✓ Identity verified (risk score: 12)
✓ Bank account linked: Chase Bank ****1234
✓ Payment processed: $50.00 (PAID)
Demo complete! Watch the dashboard for real-time updates.
To test webhook functionality:
-
Set up ngrok or similar tunnel:
ngrok http 3001
-
Configure webhook in Straddle dashboard:
- Point to:
https://your-ngrok-id.ngrok.io/api/webhooks/straddle - Get your webhook secret from the dashboard
- Point to:
-
Update
.env:WEBHOOK_SECRET=whsec_xxx_from_straddle NGROK_URL=https://your-ngrok-id.ngrok.io
Webhooks use Svix signature verification. Requests with missing or invalid signatures are rejected before any state changes.
The demo includes feature flags with safe defaults for open-source:
ENABLE_UNMASK=false- Hides customer unmask functionality. Only enable if your API key hasshow_sensitivepermissions.ENABLE_LOG_STREAM=false- Disables verbose developer log stream. Set totruefor internal demos.
These flags are returned by /api/config and automatically applied in the UI.
This demo uses real Straddle API calls in sandbox mode—no mocks, just deterministic outcomes via sandbox_outcome parameters.
┌─────────────────────────────────────────────────────┐
│ Browser (localhost:5173) │
│ ┌──────────────┐ ┌──────────────────────────────┐ │
│ │ Terminal │ │ Dashboard │ │
│ │ (unified) │ │ (Customer, Paykey, Charge) │ │
│ │ + API Logs │ │ │ │
│ └──────────────┘ └──────────────────────────────┘ │
└─────────────┬───────────────────────────────────────┘
│ fetch() API calls
▼
┌─────────────────────────────────────────────────────┐
│ Express Server (localhost:3001) │
│ ┌──────────┐ ┌────────────┐ ┌─────────────────┐ │
│ │ Routes │→ │ Straddle │→ │ Sandbox API │ │
│ │ │ │ SDK │ │ │ │
│ └──────────┘ └────────────┘ └─────────────────┘ │
│ │ │
│ ▼ SSE stream │
│ ┌──────────────────┐ │
│ │ State + Events │ │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────┘
Backend:
- Node.js 18+ with TypeScript
- Express.js REST API
- @straddlecom/straddle v0.3.0 - Official Straddle SDK
- Server-Sent Events for real-time updates
- Svix for webhook signature verification
Frontend:
- React 18 + TypeScript
- Vite for fast dev server and builds
- Tailwind CSS with custom retro 8-bit theme
- Zustand for state management
- Straddle Bridge React SDK for Plaid integration
Design:
- Retro gaming aesthetic (cyan neon, scanlines, CRT effects)
- Split-screen layout optimized for live demos
- Real-time API request visualization with syntax highlighting
nerdcon-demo/
├── server/ # Express backend with Straddle SDK
│ └── src/
│ ├── routes/ # API endpoints (customers, paykeys, charges, webhooks)
│ ├── domain/ # State management and types
│ ├── middleware/ # Request tracing, error handling
│ └── __tests__/ # Jest tests with >80% coverage
├── web/ # React frontend
│ └── src/
│ ├── components/ # Terminal, dashboard cards, UI components
│ ├── lib/ # Commands parser, state, API client
│ └── __tests__/ # Vitest tests with >80% coverage
└── design/ # Design system and retro components
This demo showcases Straddle's core APIs using SDK v0.3.0:
const customer = await straddle.customers.create({
name: "Alberta Bobbeth Charleson",
type: "individual",
email: "alberta@example.com",
phone: "+12125550123",
device: { ip_address: "192.168.1.1" },
config: { sandbox_outcome: "verified" }
});
// Access response data
console.log(customer.data.id);
console.log(customer.data.verification_status);
console.log(customer.data.risk_score);const paykey = await straddle.bridge.link.bankAccount({
customer_id: "cus_xxx",
account_number: "123456789",
routing_number: "021000021",
account_type: "checking",
config: { sandbox_outcome: "active" }
});
// IMPORTANT: Two different fields!
console.log(paykey.data.id); // Resource ID for GET /paykeys/:id
console.log(paykey.data.paykey); // TOKEN for charges.create - USE THIS!const charge = await straddle.charges.create({
paykey: "758c519d.02.2c16f91...", // Use TOKEN, not ID!
amount: 5000, // $50.00 in cents
description: "Payment for services",
currency: "USD",
consent_type: "internet",
payment_date: "2024-01-15",
device: { ip_address: "192.168.1.1" },
config: {
balance_check: "enabled",
sandbox_outcome: "paid"
}
});Control deterministic behavior with config.sandbox_outcome:
| Resource | Outcomes |
|---|---|
| Customers | standard, verified, review, rejected |
| Paykeys | standard, active, review, rejected |
| Charges | standard, paid, on_hold_daily_limit, cancelled_for_fraud_risk, cancelled_for_balance_check, failed_insufficient_funds, failed_customer_dispute, failed_closed_bank_account, reversed_insufficient_funds, reversed_customer_dispute, reversed_closed_bank_account |
See /outcomes command in the terminal for detailed descriptions of each outcome.
# Development
npm run dev # Start both server and web
npm run dev:server # Server only (port 3001)
npm run dev:web # Web only (port 5173)
# Building
npm run build # Build both workspaces
npm run build:server # Build server only
npm run build:web # Build web only
# Code quality
npm run lint # ESLint all workspaces
npm run format # Prettier formatting
npm run type-check # TypeScript validation
# Testing
npm test --workspace=server # Run server tests (Jest)
npm test --workspace=web # Run web tests (Vitest)
npm run test:coverage # Run tests with coverage reportBoth workspaces have comprehensive test coverage (>80%):
# Server tests (Jest)
cd server
npm test
# Web tests (Vitest)
cd web
npm test
# Coverage reports
npm run test:coverageThe project uses Husky and lint-staged for pre-commit checks:
- ESLint with auto-fix
- Prettier formatting
- TypeScript type checking
To bypass (not recommended):
git commit --no-verifyBackend:
- Create new Web Service
- Build command:
npm install && npm run build:server - Start command:
npm run start:server - Environment variables:
STRADDLE_API_KEY=sk_sandbox_xxx STRADDLE_ENV=sandbox CORS_ORIGIN=https://your-frontend-url.com WEBHOOK_SECRET=whsec_xxx ENABLE_UNMASK=false ENABLE_LOG_STREAM=false
Frontend:
- Create new Static Site
- Build command:
npm install && npm run build:web - Publish directory:
web/dist - Add environment variable:
VITE_API_URL=https://your-backend-url.onrender.com
Webhooks:
- Configure in Straddle dashboard:
https://your-backend-url.onrender.com/api/webhooks/straddle
- Straddle Documentation: docs.straddle.com
- API Reference: docs.straddle.com/llms.txt
- Node SDK: github.com/straddleio/straddle-node
- Developer Guide: See CLAUDE.md for detailed implementation guide
Contributions are welcome! Please read the Developer Guide for:
- Code architecture and patterns
- Testing guidelines
- Common development tasks
- Troubleshooting tips
MIT License - See LICENSE for details.
- Check the Developer Guide for technical details
- Visit docs.straddle.com for API documentation
- Open an issue for bugs or feature requests
- The code is designed to be readable and educational - explore it!
Built with ⚡ by the Straddle team