Platform Overview
Everything FoxCalc does — architecture, features, integrations, customization, and security.
Table of Contents
- What Is FoxCalc
- How It Works
- Integration Path
- The Calculator Experience
- The Funder Portal
- API Reference Summary
- Customization & Configuration
- Formula Engine
- Prepayment Models
- Fee Configurability
- Webhooks & Notifications
- Attachment Handling
- Security Architecture
- Roles & Permissions
- Audit & Compliance
- Infrastructure & Reliability
What Is FoxCalc
FoxCalc is a multi-tenant MCA pricing calculator platform. Funders send deal parameters via API, FoxCalc hosts a branded calculator experience for ISOs (Independent Sales Organizations), and delivers the ISO's configured terms back to the funder via webhooks.
Key distinction: FoxCalc is a platform, not a single-client tool. Every funder gets their own isolated environment — their own brands, their own field labels, their own fee structures, their own callback endpoints, their own user roles. Nothing in the system is hardcoded to any single funder. Funder-specific behavior lives entirely in an adapter layer.
What Problem It Solves
Funders approve MCA deals with ranges — a purchase price between $20K and $50K, a term between 6 and 18 months, an upsell up to 5%. Today, communicating those ranges to ISOs is manual: spreadsheets, PDFs, phone calls. ISOs configure terms by guessing, back-and-forth emails follow, deals slow down.
FoxCalc replaces that with a live, interactive calculator the ISO opens in their browser. They drag sliders, see every number update in real time, and submit their configured terms with one click. The funder gets a structured callback within seconds.
Who Uses It
| Role | What They Do |
|---|---|
| Funder (API) | Sends deal parameters to FoxCalc via REST API |
| Funder (Portal) | Manages deals, brands, users, settings, and monitors activity |
| ISO | Opens the calculator link, configures terms, uploads stipulations, submits |
How It Works
The Deal Lifecycle
Funder sends deal via API
↓
Offer created (draft or active)
↓
Underwriter reviews in Portal (optional)
↓
Deal released → signed calculator URL generated
↓
ISO opens calculator → configures terms interactively
↓
ISO submits → FoxCalc computes final terms
↓
Webhook fires → funder receives structured callback
↓
Offer status → submitted (terminal)Offer Statuses
| Status | Meaning |
|---|---|
draft | Created but not yet released. ISO cannot access. Underwriter can preview. |
active | Released. Calculator URL is live. ISO can configure and submit. |
submitted | ISO submitted terms. Terminal state. |
expired | Past expiration date. ISO sees expiration notice. |
revoked | Manually cancelled by funder. ISO sees revocation notice. |
Valid transitions: draft → active → submitted, draft → revoked, active → expired, active → revoked
Integration Path
Phase 1 — Platform Setup (~1 day, FoxCalc team)
- Create funder record with slug (e.g.,
fox,forward) - Create first admin user (sends invite email)
- Configure SendGrid email infrastructure
Phase 2 — Funder Self-Service (~1–3 days)
- Admin logs in, sets password via invite link
- Creates brand(s) — logo, colors, display name
- Adds DNS records for email domain (SPF/DKIM/DMARC)
- Generates API key(s) with scopes
- Configures field mapping (funder's field names → FoxCalc canonical schema)
- Sets organization defaults — fees, stipulations, prepayment model, guardrails, terminology
- Configures callback URL + secret
- Subscribes to webhook events
- Invites underwriter users
Phase 3 — Integration Testing (~1–5 days)
- Funder sends test deals via API (draft status)
- Underwriter reviews drafts, configures guardrails, previews calculator
- End-to-end validation — release, open calculator, submit, verify callback
- Funder confirms integration
Phase 4 — Go Live
- Funder starts sending production deals
- FoxCalc team monitors first batch
The Calculator Experience
The calculator is a React single-page application served from a signed URL. No login, no accounts, no sessions. The ISO clicks the link and starts configuring.
What the ISO Sees
Header:
- Funder's brand logo and name
- Merchant name, ISO name
- Deal type badge, approval date
- Expiration countdown (red when approaching)
- Funder notes (expandable)
Left Panel — Interactive Controls:
- Purchase price slider + text input (within funder-defined floor/ceiling)
- Estimated payments slider + text input (within funder-defined range)
- Frequency toggle — daily, weekly, monthly (available options set by funder)
- Upsell slider (0% to funder's max)
- Adjustable fees — each fee the funder marks as adjustable gets its own slider with floor/ceiling
- ISO notes — free text field (when enabled by funder)
Right Panel — Computed Terms (read-only, updates in real time):
- Purchase price, buy rate, total factor rate, amount sold
- Payment amount with frequency label (e.g., "$3,650/week")
- Number of payments, estimated term
- Commission (rate + dollar amount)
- Total fees, existing funding balance, net to merchant
- Holdback percentage, position
- Prepayment schedule table (when enabled)
- Stipulations list with file upload
Three Dynamic Constraints
The calculator enforces three constraints simultaneously via slider range adjustments — no error modals, no blocked states:
| Constraint | What It Does |
|---|---|
| Payment ceiling | As upsell increases or payments decrease, purchase price ceiling tightens to keep payment under cap |
| Net-to-merchant floor | As fees increase, purchase price floor rises to prevent negative net |
| Fee ceiling | At low purchase prices, adjustable fee ceilings tighten to keep net ≥ 0 |
Submission Flow
- ISO clicks "Request Contracts"
- Confirmation modal shows all configured terms (layout and visible fields customizable by funder)
- Pending file uploads complete automatically
- ISO clicks "Confirm & Submit"
- Server validates version (optimistic concurrency), computes final terms, stores submission
- Webhook fires to funder with complete structured data
- ISO sees success screen
Calculator States
| State | What the ISO Sees |
|---|---|
| Active | Full interactive calculator |
| Preview | Yellow banner, all controls work, submit disabled |
| Expired | Expiration notice with date |
| Revoked | Cancellation notice |
| Submitted | "Already submitted" notice |
| Invalid link | Signature mismatch error |
Brand Theming
Every calculator renders with the funder's brand identity:
- Logo in header
- Primary color on CTAs, slider accents, active states
- Accent color on highlights
- Custom field labels via terminology overrides
- All applied as CSS custom properties at runtime — same layout, different skin
The Funder Portal
A Next.js admin application where funders manage everything. JWT session auth with role-based access control.
Deal Management
Deal List:
- Paginated with search (merchant name, ISO name, deal ID)
- Filter by status, brand, date range
- Sort by created date, merchant name, status, expiration
- Click through to deal detail
Deal Detail:
- Full deal parameters — all editable while in draft/active status
- Three-card layout: payment structure, pricing constraints, merchant data
- Fee editor with adjustability toggles and ranges
- Stipulations, notification emails, assigned users
- Preview button (opens calculator in new tab with submit disabled)
- Release button (draft → active, generates signed URL)
- Revoke button (cancels deal)
- Activity timeline — every lifecycle event with timestamps
- Webhook delivery logs — every callback attempt with status codes
- Submission details (if submitted) — all ISO selections, computed terms, attachments with download links
Manual Deal Creation:
- Full form pre-populated with funder defaults
- Create as draft or active
- All fields from the canonical offer schema
API Simulator:
- Build a deal payload using the same form
- Live JSON preview (snake_case, ready to paste)
- Copy as JSON or cURL
- "Simulate" button — validates, shows defaults that would be applied, shows final payload
- "Create This Deal" button — creates from simulation
Settings (13 Pages)
| Setting | What It Controls |
|---|---|
| Brand & Appearance | Logo, colors, display name, email identity. Multi-brand support. |
| Callbacks | Per-brand callback URL + secret. Ping test button. |
| Fee Templates | Default fee structures (fixed/percentage, visible/hidden, adjustable/fixed, floor/ceiling) |
| Roles & Permissions | Editable role × permission matrix. Customized roles per funder. |
| Domains & DNS | Custom calculator domains (CNAME), email DNS records (SPF/DKIM/DMARC) |
| Webhooks | Event subscriptions per brand. 13 event types. Active/paused toggle. |
| API Keys | Generate, revoke, search. Scopes and IP allowlist. Secret shown once. |
| Calculator Display | Toggle prepayment schedule, stipulations, fee breakdown visibility |
| Guardrails | Default frequencies, payment ranges, max upsell, payment override behavior |
| Deal Defaults | Expiration policy, notification emails, assigned users, prepayment model, terminology overrides, rate limits |
| Submission Form | Confirmation modal layout (1 or 2 column), header message (rich text with variables), field visibility/order/accent |
| Users | Invite, deactivate, role assignment, bulk CSV upload, login history |
| User Detail | Per-user permission overrides, recent deals, login history |
User Management
- Invite by email (sends branded invite with 7-day expiration)
- Bulk invite via CSV upload (up to 50 users, validation preview)
- Six roles with 20 permissions across 7 categories
- Per-user permission overrides (grants/revokes on top of role)
- Login history per user (timestamp, IP, event type, success/failure)
API Reference Summary
Three Route Namespaces
Funder API (/api/v1/{funder}/...) — HMAC signature auth
| Method | Endpoint | Purpose |
|---|---|---|
| POST | /api/v1/{funder}/offers | Create new offer |
| PUT | /api/v1/{funder}/offers/{deal_id} | Replace offer (full overwrite) |
| PATCH | /api/v1/{funder}/offers/{deal_id} | Partial update |
| DELETE | /api/v1/{funder}/offers/{deal_id} | Revoke offer |
Calculator API (/api/v1/offers/...) — Signed URL auth
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /api/v1/offers/{deal_id} | Fetch offer for calculator |
| POST | /api/v1/offers/{deal_id}/submit | Submit configured terms |
| POST | /api/v1/offers/{deal_id}/upload-urls | Get presigned S3 upload URLs |
| POST | /api/v1/offers/{deal_id}/confirm-upload | Confirm file uploaded |
| POST | /api/v1/offers/{deal_id}/delete-attachment | Remove attachment |
Portal API (/portal/api/...) — JWT session auth, ~38 endpoints
Covers: authentication, deal CRUD, brand management, API keys, user management, webhook subscriptions, audit log, roles & permissions, callbacks, field mapping, funder config.
Authentication Mechanisms
| Surface | Method | Details |
|---|---|---|
| Funder → API | HMAC-SHA256 | X-API-Key + X-Signature + X-Timestamp. 5-minute replay window. Per-key scopes and IP allowlist. |
| ISO → Calculator | Signed URL | Query-param HMAC signature + expiration. No login, no session. |
| Portal | JWT | httpOnly cookies, 15-min access + 7-day refresh. Role-based permissions. |
| Funder ← Callback | HMAC-SHA256 | X-FoxCalc-Signature + X-FoxCalc-Timestamp on outbound webhooks. |
Optimistic Concurrency
Every offer has a version integer. ISO includes version in submission. Portal includes version in edits. Version mismatch → 409 Conflict with instruction to reload.
Customization & Configuration
Per-Funder Configuration
Every funder can customize:
| Area | What's Configurable |
|---|---|
| Branding | Multiple brands per funder. Each brand has logo, primary/accent colors, display name, email identity, custom domain. |
| Terminology | Every field label is overridable. "Purchase Price" can become "Funding Amount", "Amount Sold" can become "RTR". JSONB on brands/funders table. |
| Fees | Fixed or percentage. Visible or hidden. Adjustable or locked. Floor/ceiling on adjustable fees. Unlimited custom fee IDs. |
| Guardrails | Purchase price range, payment range, max upsell, payment override behavior (ceiling/fixed/none), base and available frequencies. |
| Prepayment | Three models: step-per-month (auto-generated), fixed windows, or funder-provided table. All configurable per deal. |
| Stipulations | Default stipulation list. Per-deal overrides. File upload with MIME validation and size limits. |
| Callbacks | Per-brand default. Per-deal override. HMAC signing secret. 8-attempt retry with exponential backoff. |
| Webhooks | 13 event types. Per-brand or funder-wide subscriptions. HMAC signature verification. |
| Display | Toggle visibility: prepayment schedule, stipulations, fee breakdown. |
| Submission form | Layout (1 or 2 column), header message (rich text with {{merchant_name}}), field visibility/order/accent for 15 fields. |
| Rate limits | Per-funder API limits (requests per minute/hour/day). |
| Roles | Customize permission matrix per role per funder. Per-user permission overrides. |
| Expiration | Default expiration days. Per-deal override. Auto-expiration cron. |
| Per-brand from name and from address. DNS records for deliverability. | |
| Adapter | Inbound field mapping (funder's field names → canonical). Outbound payload format (per-funder adapter). |
Defaults Hierarchy
Funder defaults apply when offer-level data is missing:
Offer data (highest priority)
↓
Brand defaults
↓
Funder defaults (lowest priority)Five default categories: fees, stipulations, prepayment config, guardrails, display config. Offer-level always wins. Arrays merge only when offer array is empty.
Formula Engine
All calculations live in @foxcalc/shared — imported by both calculator (client-side) and portal (server-side) to guarantee identical math.
12 Computed Fields
| # | Field | Formula |
|---|---|---|
| 1 | Total factor rate | buy_rate + upsell |
| 2 | Amount sold | purchase_price × total_factor_rate |
| 3 | Commission rate | commission_rate_override ?? upsell |
| 4 | Commission dollars | purchase_price × commission_rate |
| 5 | Resolved fee | amount_type === "pct" ? purchase_price × amount : amount |
| 6 | Total fees | Σ resolved_fee(fee) for all fees |
| 7 | Net to merchant | purchase_price - total_fees - existing_funding_balance |
| 8 | Estimated term | round(num_payments / paymentsPerMonth[base_frequency]) |
| 9 | Payment amount (base) | amount_sold / num_payments |
| 10 | Display conversion | base_payment × freq_divisor[selected_frequency] |
| 11 | Holdback % | (daily_payment) / (daily_receivables) — dynamic when monthly receivables provided |
| 12 | Total interest | amount_sold - purchase_price |
Two Constant Sets
Term estimation (PAYMENTS_PER_MONTH):
- Daily: 21.67 (260/12), Weekly: 4.33 (52/12), Monthly: 1
Display grouping (FREQUENCY_DIVISOR):
- Daily: 1, Weekly: 5, Monthly: 22.5
Frequency switching regroups payments without changing economics. Total repayment and term stay identical.
Prepayment Models
Three models supported simultaneously. Funder chooses per deal.
Model A — Step-Per-Month
Auto-generates a tier for every month from base term down to a floor. Factor rate steps down by a fixed amount per month.
Example: Base term 12 months, factor rate 1.38, rate step 0.02, floor 3 months → generates 10 tiers, each month earlier saves 2 points on the factor rate.
Tiers regenerate when ISO adjusts term or upsell. Number of tiers changes with term.
Model B — Fixed Windows
Fixed prepayment windows measured in months after funding. Each window has a specific discount in factor rate points.
Example: Month 1: 10pts off, Month 2: 7pts off, Month 3: 5pts off. Windows don't change when ISO adjusts term — they're relative to funding date.
Model C — Funder-Provided
Pre-computed table from funder's system. Dollar amounts are static; factor rates recalculate with ISO adjustments.
Uniform Output
All models produce the same PrepaymentTier structure: label, term/window, factor rate, amount sold, savings, discount points. The calculator renders them identically regardless of model.
Fee Configurability
Each deal can have unlimited fees. Each fee has independent configuration:
| Property | Options | Effect |
|---|---|---|
| ID | Any string (origination, underwriting, wire, custom) | Unique identifier |
| Label | Any string | Display name (subject to terminology overrides) |
| Amount | Dollar value (fixed) or decimal (pct) | 0.03 = 3% for percentage fees |
| Type | fixed or pct | Fixed = dollars, Pct = percentage of purchase price |
| Visible | true / false | Hidden fees still deducted but not shown to ISO |
| Adjustable | true / false | ISO can change within floor/ceiling |
| Floor/Ceiling | Same unit as type | Bounds for ISO adjustment |
Dynamic ceiling enforcement: At low purchase prices, adjustable fee ceilings automatically tighten to prevent net-to-merchant going negative. The ISO never sees an error — the slider range adjusts.
Webhooks & Notifications
13 Webhook Event Types
| Event | When It Fires |
|---|---|
offer.created | New offer via API |
offer.replaced | Full overwrite via PUT |
offer.patched | Partial update via PATCH |
offer.revoked | Cancelled via DELETE |
offer.expired | Past expiration date (cron) |
calculator.opened | ISO loaded calculator |
submission.received | ISO submitted terms |
submission.callback_failed | All callback retries exhausted |
email.bounced | Notification email bounced |
email.opened | ISO opened notification email |
attachment.delivered | Push delivery confirmed |
attachment.delivery_failed | Push delivery failed |
Subscription Management
- Per-brand or funder-wide subscriptions
- Event type filtering (subscribe to any subset)
- Active/paused toggle
- HMAC-SHA256 signed payloads (
X-Signature,X-Webhook-Event,X-Webhook-ID,X-Timestamp) - Portal UI for full CRUD
Callback Delivery
When an ISO submits, the primary callback fires immediately:
Payload includes: deal ID, offer version, timestamp, all ISO selections, all computed terms, prepayment schedule, attachment download URLs (presigned, 72-hour expiration).
Retry strategy (8 attempts):
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 10 seconds |
| 3 | 30 seconds |
| 4 | 1 minute |
| 5 | 5 minutes |
| 6 | 15 minutes |
| 7 | 1 hour |
| 8 | 6 hours |
After 8 failures: marked as failed, notification email sent to all deal recipients.
Email Notifications
Five email templates, all programmatically generated with brand theming:
| Trigger | Recipients | |
|---|---|---|
| Submission confirmation | ISO submits | All notification emails on the deal |
| Expiry warning | 24 hours before expiration | All notification emails on the deal |
| Callback failure | 8 failed callback attempts | All notification emails on the deal |
| User invite | Admin invites portal user | Invited email address |
| Password reset | User requests reset | User's email address |
Submission emails include a JSON summary attachment and (if under 10 MB total) all stipulation files.
Attachment Handling
Upload Flow
- ISO selects files in calculator (drag-and-drop or file picker)
- Calculator requests presigned S3 upload URLs from API
- Files upload directly from browser → S3 (never through API server)
- Calculator confirms each upload
- On submission, attachment metadata is included in the callback
Constraints
| Limit | Value |
|---|---|
| Max file size | 25 MB per file |
| Max files per stipulation | 5 |
| Max total per submission | 100 MB |
| Allowed types | PDF, PNG, JPEG, HEIC, TIFF, DOC, DOCX, XLS, XLSX, CSV |
MIME type validated on upload (extensions are not trusted).
Delivery to Funder
Pull model: Callback includes presigned download URLs (72-hour default expiration, configurable per funder). Funder downloads at their pace.
Push model (planned): Platform pushes files to funder's system — multipart POST, SFTP, or S3-to-S3 copy.
Storage
S3, encrypted at rest (AES-256). Bucket structure: {funder_id}/{deal_id}/{stipulation_id}/{filename}. Retention configurable per funder.
Security Architecture
Four Authentication Boundaries
| Boundary | Method | Protection |
|---|---|---|
| Funder → API | HMAC-SHA256 | Request-level integrity. Timestamp replay protection (±5 min). Per-key scopes. Optional IP allowlist. |
| Calculator access | Signed URLs | HMAC signature + expiration in query params. No session, no cookies. |
| Portal | JWT sessions | httpOnly secure cookies. 15-min access token, 7-day refresh. |
| API → Funder | HMAC-SHA256 | Signed outbound webhooks. Per-brand secrets. |
Input Validation
- All API inputs validated via Zod schemas
- SQL injection prevented via Drizzle ORM (parameterized queries)
- XSS prevented via React auto-escaping
- File upload MIME validation (content-based, not extension-based)
- Webhook URLs validated against private IP ranges (SSRF protection)
Security Headers
Every API response includes:
Strict-Transport-Security(HSTS with preload)X-Frame-Options: DENYX-Content-Type-Options: nosniffContent-Security-Policy(dynamic, scoped to allowed origins)Referrer-Policy: strict-origin-when-cross-origin
Secrets Management
- AWS Secrets Manager for production credentials
- No secrets in environment variables, config files, or code
- API key secrets stored as SHA-256 hashes (never in plaintext)
- Full key shown exactly once at creation
Rate Limiting
| Surface | Default Limits |
|---|---|
| Funder API | 500 req/min, 50 req/sec (burst) |
| Calculator loads | 30/min per deal |
| Submissions | 5/hour per deal |
| Upload URLs | 30/hour per deal |
| Global | 1,000 req/sec |
Per-funder overrides via rateLimits JSONB. Redis sliding windows with graceful degradation.
Roles & Permissions
Six Roles
| Role | Access Level |
|---|---|
| Platform Admin | All permissions. Immutable. FoxCalc team only. |
| Admin | Full organization access — deals, settings, users, keys, webhooks, audit. |
| Underwriter | Deal management — view all, edit, release, revoke, assign. Read-only settings. |
| ISO RM Manager | View all deals + activity. Read-only. |
| ISO RM | View own assigned deals only. |
| API Only | No portal access. API key usage only. |
20 Permission Atoms
Organized across 7 categories: Deals (9), Settings (2), Users (3), API Keys (3), Webhooks (1), Audit (1), Reports (1).
Three-Layer Resolution
- Baseline — hardcoded per-role defaults
- Funder customization — editable matrix in portal (per funder)
- User overrides — per-user grants/revokes on top of role
Audit & Compliance
Audit Log
Every administrative action logged with:
- Actor (portal user, API key, or system)
- Action type (CRUD operations on any resource)
- Resource type and ID
- Request metadata (IP, user agent)
- Sanitized payload (13 sensitive patterns scrubbed)
- Timestamp (UTC)
Portal UI: paginated, filterable by action, actor, resource, date range.
Deal Activity Log
Every deal lifecycle event:
- Calculator opened (IP, user agent)
- Offer created, replaced, patched, revoked, expired
- Submission received
- Callback attempts and results
Login History
Per-user login tracking:
- Login, logout, token refresh, failed login
- IP address, user agent, session ID
- Success/failure with reason
- Portal UI with pagination and filters
Offer History
Full version history with before/after snapshots:
- Every create, replace, patch, revoke
- Changed field lists
- Actor tracking (API key or portal user)
Compliance Engine (Planned)
Stubbed for V2: APR calculation, state-specific disclosures (NYDFS, CA SB 1235, Virginia, Utah). Funder-computed compliance data passthrough supported in V1.
Infrastructure & Reliability
Tech Stack
| Layer | Technology |
|---|---|
| Backend | Node.js, TypeScript, Fastify |
| Calculator | React 19, Vite |
| Portal | Next.js 15 |
| Database | PostgreSQL 16, Drizzle ORM |
| Cache/Queue | Redis 7, BullMQ |
| Storage | S3 (encrypted at rest) |
| SendGrid | |
| Hosting | AWS (ECS Fargate, RDS, ElastiCache, CloudFront, S3) |
| CI/CD | GitHub Actions (4 workflows) |
Architecture
Monorepo with 4 packages sharing a single formula engine:
@foxcalc/shared— types, formulas, constants (imported by all)@foxcalc/api— Fastify backend@foxcalc/calculator— React SPA@foxcalc/portal— Next.js admin app
Calculator and API share a domain via CloudFront path-based routing: /* → S3 (calculator), /api/* → ALB (API). Zero CORS anywhere.
Database
15 tables, 19 migrations, optimistic concurrency via version integers. All timestamps UTC (ISO 8601). JSONB for flexible payloads alongside structured columns.
Background Workers
- Callback delivery — BullMQ, 5 concurrent, 8 retries with exponential backoff
- Webhook dispatch — BullMQ, 10 concurrent, fire-and-forget
- Expiration cron — every 5 minutes, flips active offers past expiration date
- Expiry warnings — every 5 minutes, emails for deals expiring within 24 hours
Testing
| Package | Tests |
|---|---|
| Shared | 108 unit tests (formulas, types, constants) |
| API | 422 unit + integration tests |
| Calculator | 22 unit tests |
| Portal | 239 E2E Playwright tests |
| Total | 791 tests |
Adapter Pattern
All funder-specific behavior is isolated in three adapter boundaries:
| Adapter | Responsibility |
|---|---|
| Inbound | Field mapping, auth, payload normalization |
| Outbound | Callback format, delivery method, signing |
| Notification | Email templates, branding, delivery channel |
Adding a new funder means writing three adapter files. Zero changes to core platform code. "That's a config change" is the right answer to every funder request.