Spec: PSX-to-SaaS BestEx Integration
Status: Draft Author: Kevin Sawyer (Product Owner) + AI Architect Date: 2026-03-17
Overview
Enable PowerSeller X to call the PSSaaS BestEx API, allowing principals to compare agency delivery scenarios (from BestEx) alongside marketplace pricing (from PSX) in a single unified analysis. PSX sends a batch of loans, PSSaaS runs BestEx against the principal's configured profiles, and returns ranked scenarios without permanently storing the loans.
Business Context
A mortgage lender's secondary marketing desk faces a daily decision for every loan: deliver to an agency commitment, sell via PSX, or hold. Today these analyses happen in separate systems. This integration eliminates that gap — PSX becomes the single pane of glass for best-path analysis.
This is particularly valuable for MWFI Holdings (Watermark TPO), who will be both the first PSX principal and the first PSSaaS tenant, making them the natural pilot for this integration.
PowerBuilder Reference
The legacy PowerSeller Desktop App had no equivalent to this integration — it was a standalone system. This is a net-new capability enabled by the SaaS architecture.
Requirements
PSSaaS Side (This Project)
- Create integration endpoint
POST /api/bestex/integration/analyze - Accept simplified loan payload (subset of full loan table — only BestEx-relevant fields)
- Accept
profileNameto determine which BestEx configuration to use - Write submitted loans to a temporary working set (not the tenant's permanent loan table)
- Run the standard 24-step BestEx pipeline against the temporary loans
- Return ranked results with the top N scenarios per loan (default: top 5)
- Return errors for loans that couldn't be analyzed
- Clean up temporary data after returning results
- Authenticate via OAuth2 client credentials (machine-to-machine)
- Resolve tenant from the client credential's tenant claim
- Rate-limit integration calls (prevent abuse — configurable, default 10 requests/minute)
- Log all integration calls for audit (caller, tenant, loan count, duration)
PSX Side (Separate Project)
- Register as an API client in PSSaaS identity provider
- Map
NormalizedLoanfields to PSSaaS integration loan format - Call PSSaaS integration endpoint after PSX pricing completes
- Merge PSSaaS BestEx results with PSX pricing results
- Present unified best-path comparison to the principal
- Handle PSSaaS unavailability gracefully (PSX analysis still works without BestEx)
- Cache principal's PSSaaS tenant mapping
Data Mapping
PSX (NormalizedLoan) | PSSaaS Integration Payload | Notes |
|---|---|---|
loan_id | loanId | Pass-through |
upb or loan_amount | loanAmount | Decimal |
note_rate | noteRate | Decimal, 6 places |
product_type + term | instrumentName | Mapped via config (e.g., "CONV" + 30 → "FNMA 30yr Fixed") |
property_state | state | 2-letter code |
lock_expiration | lockExpirationDate | ISO date |
note_date or close_date | closeDate | ISO date |
fico | creditScore | Integer |
ltv | ltv | Decimal, 3 places |
cltv | cltv | Decimal, 3 places |
purpose | purposeCode | Mapped (e.g., "P" → "Purchase") |
occupancy | occupancyCode | Mapped (e.g., "O" → "PrimaryResidence") |
property_type | propertyType | Mapped (e.g., "SF" → "SFR") |
amort_type | amortType | Mapped |
units | numOfUnits | Integer |
product_code | productCode | Pass-through or mapped |
Integration Loan Payload
interface IntegrationLoan {
loanId: string;
loanAmount: number;
noteRate: number;
instrumentName: string;
state: string;
lockExpirationDate?: string;
closeDate?: string;
creditScore?: number;
ltv?: number;
cltv?: number;
purposeCode?: string;
occupancyCode?: string;
propertyType?: string;
amortType?: string;
numOfUnits?: number;
productCode?: string;
}
interface IntegrationRequest {
profileName: string;
loans: IntegrationLoan[];
maxRanksPerLoan?: number; // default: 5
}
interface IntegrationResponse {
runDate: string;
loansProcessed: number;
scenariosGenerated: number;
errorCount: number;
duration: string;
results: IntegrationResult[];
errors: IntegrationError[];
}
interface IntegrationResult {
loanId: string;
rank: number;
investorId: string;
investorInstrumentName: string;
totalPrice: number;
profitPrice: number;
profitAmount: number;
basePrice: number;
priceAdjustment: number;
servicingFee: number;
guaranteeFee: number;
pricingWindowInDays: number;
lockWindowInDays: number;
settlementDate: string;
servicingType: string;
}
interface IntegrationError {
loanId: string;
errorDescription: string;
instrumentName?: string;
}
Business Rules
- Integration loans are ephemeral — they exist only for the duration of the analysis run and are deleted after results are returned
- The integration uses the same 24-step pipeline as direct BestEx — no shortcuts or simplified calculations
- Results are filtered to top N ranks per loan (configurable, default 5) to reduce payload size
- The integration endpoint must respond within 60 seconds for up to 500 loans
- If the profile doesn't exist or isn't properly configured, return HTTP 400 with a clear error message
- If no prices are loaded for the profile's instruments, return HTTP 422 with a "no pricing data" error
- Integration calls do NOT trigger archiving (archive_results flag is ignored for integration runs)
- Integration calls DO count toward the tenant's usage metrics
API Design
PSSaaS Endpoint
POST /api/bestex/integration/analyze
Authorization: Bearer <client_credentials_token>
X-Tenant-Id: <principal_tenant_id>
Content-Type: application/json
{
"profileName": "psx-default",
"loans": [ ... ],
"maxRanksPerLoan": 5
}
Responses:
200 OK— analysis complete, results in body400 Bad Request— invalid profile or request format401 Unauthorized— invalid or missing credentials422 Unprocessable Entity— profile exists but can't run (no prices, no instrument mappings)429 Too Many Requests— rate limit exceeded500 Internal Server Error— unexpected failure
UX Notes
This integration has no direct UI in PSSaaS. The UI is on the PSX side, where the principal sees a unified comparison table:
| Loan ID | Best Agency Path (BestEx) | Best PSX Path | Best Bulk Path | Recommended |
|---|---|---|---|---|
| L-001 | FNMA 30yr @ 101.25 (+$4,375) | PennyMac @ 101.50 (+$5,250) | — | PSX |
| L-002 | FHLMC 15yr @ 102.00 (+$3,000) | — | BulkCo @ 101.75 (+$2,625) | Agency |
The PowerSeller X team will design this UI as part of their principal portal.
Acceptance Criteria
-
POST /api/bestex/integration/analyzeaccepts a batch of loans and returns ranked BestEx results - Results match what a direct BestEx run would produce for the same loans and profile
- Temporary loans are not visible in the tenant's loan table after the call completes
- The endpoint responds within 60 seconds for 500 loans with 4 instrument mappings and 3 lock windows
- Invalid profile returns 400 with descriptive error
- Missing pricing data returns 422 with descriptive error
- Unauthenticated requests return 401
- Rate limiting works (429 after exceeding threshold)
- Integration calls are logged with caller, tenant, loan count, and duration
Out of Scope
- PSX-side implementation — the mapping, calling, and UI integration are the PowerSeller X project's responsibility, not this spec
- Real-time pricing push — PSSaaS does not push price updates to PSX; PSX calls PSSaaS on-demand
- Bidirectional data sync — loans do not flow from PSSaaS to PSX or vice versa permanently
- Identity provider setup — configuring Keycloak/Azure AD for client credentials is infrastructure work, not application code
- PSX UI design — the comparison table and principal portal are designed in the PowerSeller X project