@psifi/sdk-node
v1.112.18
Published
PsiFi Node.js SDK - Shared utilities for PsiFi services
Maintainers
Readme
@psifi/sdk-node
PsiFi Node.js SDK - Shared utilities for all PsiFi backend services.
Installation
# npm
npm install @psifi/sdk-node
# pnpm
pnpm add @psifi/sdk-node
# Internal services (pnpm workspaces)
# In package.json: "@psifi/sdk-node": "workspace:*"Note: Requires .npmrc with auth token for private package access.
Quick Start
import {
calculateBridgeFee,
validateIncomingAmount,
roundFeeUp,
roundAmountDown,
truncateToCents,
MINIMUM_PROCESSABLE_AMOUNT,
} from '@psifi/sdk-node';
// Validate incoming transaction amount
const validation = validateIncomingAmount(119.5401);
if (!validation.shouldProcess) {
console.log(`Skip: ${validation.reason}`);
return;
}
// Calculate fee on truncated amount
const result = calculateBridgeFee(validation.amount, 8.5);
console.log(`Fee: $${result.feeAmount}, Net: $${result.netAmount}`);
// Fee: $10.17, Net: $109.37Why Bridge-Compatible Rounding?
PsiFi processes USDC through Bridge API which has specific precision requirements:
- USDC has 6 decimal places (e.g.,
100.123456) - Bridge only processes whole cents (2 decimal places)
- Fractional cents are truncated (NOT rounded)
- Fees are rounded UP to ensure sufficient balance
Without proper rounding, we had issues like $0.001 deposits being charged $0.01 fees (1000% rate!).
Core Functions
truncateToCents(amount)
Truncates to whole cents. Fractional cents are discarded, not rounded.
truncateToCents(100.119999) // 100.11 (NOT 100.12)
truncateToCents(100.999999) // 100.99 (NOT 101.00)
truncateToCents(0.009) // 0.00 (sub-cent discarded)
truncateToCents(0.019) // 0.01roundFeeUp(fee)
Rounds fees UP to nearest cent. Any positive amount becomes at least 1 cent.
roundFeeUp(0.001) // 0.01 (any fractional → at least 1 cent)
roundFeeUp(0.10011) // 0.11
roundFeeUp(100.001) // 100.01
// Handles JS floating point issues:
roundFeeUp(10000 * 0.085) // 850.00 (not 850.01)roundAmountDown(amount)
Rounds amounts DOWN to nearest cent. Used for net amounts.
roundAmountDown(99.999) // 99.99
roundAmountDown(100.001) // 100.00
// Handles JS floating point issues:
roundAmountDown(99.99999999999999) // 100.00calculateBridgeFee(amount, feePercentage)
Main function for all fee calculations. Implements complete Bridge policy.
// Bridge docs example
const result = calculateBridgeFee(100100.119999, 0.1);
// {
// skip: false,
// truncatedAmount: 100100.11, // Step 1: Truncate
// feeAmount: 100.11, // Step 2: Fee (0.1%), round UP
// netAmount: 100000.00, // Step 3: Net amount
// feePercentage: 0.1,
// effectiveFeePercentage: 0.1
// }
// Dust amount - should be skipped
const dust = calculateBridgeFee(0.001, 8.5);
// { skip: true, reason: 'amount_below_minimum' }
// Fee exceeds amount
const tooSmall = calculateBridgeFee(0.01, 8.5);
// { skip: true, reason: 'fee_exceeds_amount' }validateIncomingAmount(amount)
Validate and truncate incoming amounts. Use in webhook handlers.
const validation = validateIncomingAmount(data.amount);
if (!validation.shouldProcess) {
console.log(`Skipping: ${validation.reason}`);
return res.status(200).json({ skipped: true });
}
// Use the truncated amount
const amount = validation.amount;calculateInputForDesiredOutput(desiredOutput, feePercentage)
Calculate input needed for a desired output after fees.
// Want customer to receive $100,000 after 0.1% fee
calculateInputForDesiredOutput(100000, 0.1) // 100100.11N-Way Fee Distribution
The Problem: When processing transactions with multiple fee recipients, calculating fees independently and rounding each UP can cause the total to exceed the incoming amount.
Example of the bug:
// $10 incoming with 5.4% + 3.8% + 0.9% fees = 10.1% total
// Independent rounding:
// Treasury: $10 × 5.4% = $0.54 → roundUp → $0.54
// ISO: $10 × 3.8% = $0.38 → roundUp → $0.38
// Router: $10 × 0.9% = $0.09 → roundUp → $0.09
// User payout: calculated separately as $8.9999
// Total: $0.54 + $0.38 + $0.09 + $8.9999 = $10.0099 > $10.00 ❌The Solution: calculateFeeDistribution() calculates the remainder (merchant/user payout) as incoming - sum(all fees), ensuring the total never exceeds the incoming amount.
calculateFeeDistribution(incomingAmount, feeRecipients, options)
Centralized n-way fee distribution with Bridge-compatible rounding.
Parameters:
incomingAmount- The incoming transaction amountfeeRecipients- Array of{ name, percentage, priority }(NOT including remainder party)options.remainderName- Name for remainder party (default:'merchant')options.dustThreshold- Amount to leave in source (default:0)
Returns: Object with all values needed for transfers:
byName- Object with amounts keyed by recipient name (e.g.,{ treasury: 0.54, iso: 0.38, merchant: 8.99 })distributions- Full array with metadatatotalFees,remainderAmount,totalDistributed,isValid
import { calculateFeeDistribution } from '@psifi/sdk-node';
// 4-way split: Treasury + ISO + Router + User (remainder)
const result = calculateFeeDistribution(10.00, [
{ name: 'treasury', percentage: 5.4, priority: 1 },
{ name: 'iso', percentage: 3.8, priority: 2 },
{ name: 'router', percentage: 0.9, priority: 3 },
], { remainderName: 'user' });
// result.byName = {
// treasury: 0.54, // $10 × 5.4%, rounded UP
// iso: 0.38, // $10 × 3.8%, rounded UP
// router: 0.09, // $10 × 0.9%, rounded UP
// user: 8.99 // REMAINDER: $10 - $1.01 = $8.99 ✓
// }
// result.totalFees = 1.01
// result.totalDistributed = 10.00 // Never exceeds incoming!
// result.isValid = true
// Use the values directly:
await transferToTreasury(result.byName.treasury);
await transferToIso(result.byName.iso);
await transferToRouter(result.byName.router);
await transferToUser(result.byName.user);FeeDistributionPatterns
Pre-built patterns for common scenarios:
import { FeeDistributionPatterns } from '@psifi/sdk-node';
// 2-way: Treasury + Merchant
const twoWay = FeeDistributionPatterns.twoWay(100.00, 10);
// { treasury: 10.00, merchant: 90.00 }
// 3-way: Treasury + ISO + Merchant
const threeWay = FeeDistributionPatterns.threeWayIso(100.00, 5, 3);
// { treasury: 5.00, iso: 3.00, merchant: 92.00 }
// 4-way: Treasury + ISO + Router + Merchant
const fourWay = FeeDistributionPatterns.fourWayIsoRouter(100.00, 5, 3, 1);
// { treasury: 5.00, iso: 3.00, router: 1.00, merchant: 91.00 }
// User payout (NCW)
const payout = FeeDistributionPatterns.userPayout(100.00, 8.5);
// { treasury: 8.50, user: 91.50 }Priority-Based Capping
When total fees exceed the available amount, higher priority recipients get paid first:
// $1.00 incoming with 60% + 50% fees (total 110%)
const result = calculateFeeDistribution(1.00, [
{ name: 'treasury', percentage: 60, priority: 1 }, // Higher priority
{ name: 'partner', percentage: 50, priority: 2 }, // Lower priority
]);
// Treasury (priority 1) gets full $0.60
// Partner (priority 2) gets remaining $0.40 (capped from $0.50)
// Merchant gets $0.00 (all went to fees)
// result.wasCapApplied = true
// result.byName = { treasury: 0.60, partner: 0.40, merchant: 0.00 }
// result.cappingDetails.overage = 0.10Flow Diagram: N-Way Fee Distribution
┌──────────────────────────────────────────────────────────────────────────────┐
│ N-WAY FEE DISTRIBUTION FLOW │
│ │
│ INCOMING AMOUNT │
│ $10.00 │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ Step 1: TRUNCATE TO CENTS │ │
│ │ $10.00 → $10.00 (already whole cents) │ │
│ │ $10.009 → $10.00 (fractional cents discarded) │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ Step 2: CALCULATE EACH FEE (rounded UP) │ │
│ │ │ │
│ │ Priority 1: Treasury 5.4% → $10 × 0.054 = $0.54 → roundUp → $0.54 │ │
│ │ Priority 2: ISO 3.8% → $10 × 0.038 = $0.38 → roundUp → $0.38 │ │
│ │ Priority 3: Router 0.9% → $10 × 0.009 = $0.09 → roundUp → $0.09 │ │
│ │ │ │
│ │ Total fees: $1.01 │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ Step 3: CHECK FOR OVERAGE (priority-based capping if needed) │ │
│ │ │ │
│ │ If total fees > available: │ │
│ │ - Priority 1 gets full amount first │ │
│ │ - Priority 2 gets what's left, etc. │ │
│ │ - Lower priority may be capped or zeroed │ │
│ │ │ │
│ │ Here: $1.01 ≤ $10.00 ✓ No capping needed │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ Step 4: CALCULATE REMAINDER (the key fix!) │ │
│ │ │ │
│ │ ❌ OLD WAY: Calculate user payout independently │ │
│ │ $10.00 - fees = $8.9999... (with dust handling) │ │
│ │ Total: $1.01 + $8.9999 = $10.0099 > $10.00 FAILED │ │
│ │ │ │
│ │ ✓ NEW WAY: Remainder = incoming - sum(fees) │ │
│ │ $10.00 - $1.01 = $8.99 (rounded down) │ │
│ │ Total: $1.01 + $8.99 = $10.00 ≤ $10.00 ✓ │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ FINAL DISTRIBUTION │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Treasury │ │ ISO │ │ Router │ │ User │ │ │
│ │ │ $0.54 │ │ $0.38 │ │ $0.09 │ │ $8.99 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ │ │
│ │ Total distributed: $10.00 ✓ │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────┘Validation
import { validateFeeDistribution } from '@psifi/sdk-node';
const result = calculateFeeDistribution(100, [...]);
const validation = validateFeeDistribution(result);
if (!validation.isValid) {
console.error('Distribution errors:', validation.errors);
// Possible errors:
// - EXCEEDS_INCOMING: Total > truncatedAmount
// - NEGATIVE_AMOUNT: A recipient has negative amount
// - NO_REMAINDER: Missing remainder party
}Precision Philosophy
Key principle: Maintain 6-decimal precision for intermediate values, only truncate/round at the final calculation step.
┌──────────────────────────────────────────────────────────────────────────┐
│ PRECISION THROUGH THE PIPELINE │
│ │
│ USDC has 6 decimal precision. We preserve this throughout processing: │
│ │
│ 1. INCOMING VALUE $100.123456 (6 decimals from blockchain) │
│ │ │
│ ▼ │
│ 2. INTERMEDIATE VALUES Keep 6 decimals for calculations │
│ (balances, ratios, $100.123456 ← don't truncate yet! │
│ proportional splits) │
│ │ │
│ ▼ │
│ 3. FINAL CALCULATION Now apply Bridge rounding: │
│ (fee/payout time) truncateToCents($100.123456) → $100.12 │
│ roundFeeUp(fee) → $8.51 │
│ │ │
│ ▼ │
│ 4. DISPLAY/LOGS .toFixed(2) for readability │
│ .toFixed(6) for precision audit trails │
└──────────────────────────────────────────────────────────────────────────┘Why this matters:
- Truncating too early loses precision needed for proportional calculations
- Example: 3-way split of $100.005 - need full precision to divide correctly
- Only at the final step (actual fee/payout transaction) do we apply Bridge rounding
Implementation pattern:
// ✓ CORRECT: Keep precision until final step
const grossAmount = 100.123456; // From blockchain - keep 6 decimals
const ratio = someBalance / totalBalance; // Keep full precision
const proportion = grossAmount * ratio; // Keep full precision
// Only now apply Bridge rounding for the actual transaction
const { feeAmount, netAmount } = calculateBridgeFee(proportion, feePercentage);
// ✗ WRONG: Truncating intermediate values
const truncatedEarly = truncateToCents(grossAmount); // Lost 0.003456!
const proportion = truncatedEarly * ratio; // Accumulated errorConstants
import {
// Rounding thresholds
MINIMUM_PROCESSABLE_AMOUNT, // 0.01 (1 cent)
// Operational thresholds
DUST_THRESHOLD, // 0 (disabled, reserved for future use)
MIN_SWEEP_AMOUNT, // 0.01 USDC
} from '@psifi/sdk-node';Note: Fee percentages are NOT exported from the SDK. All fee rates must come from the API server or database - never hardcoded.
Threshold Constants Explained
| Constant | Value | Purpose | Where Used |
|----------|-------|---------|------------|
| MINIMUM_PROCESSABLE_AMOUNT | 0.01 | Skip transactions < 1 cent after truncation | Fee calculations, webhook processing |
| DUST_THRESHOLD | 0 | Reserved (vault reserve, currently disabled) | Bridge vault transfers |
| MIN_SWEEP_AMOUNT | 0.01 | Min balance worth sweeping (gas costs) | Vault sweep operations |
DUST_THRESHOLD (Currently 0 - Disabled)
What it controls: Small reserve left in vaults to prevent zero-balance issues.
Status: Currently set to 0 (disabled). With Bridge rounding, all amounts are whole cents and we transfer the full balance.
Where used: Bridge onramp/offramp in webhook.routes.js
Note: This constant is kept in the codebase (set to 0) so it can be re-enabled if zero-balance vault issues occur in the future. Simply update the value in @psifi/sdk-node/constants to re-enable.
MIN_SWEEP_AMOUNT (0.01 USDC)
What it controls: Minimum vault balance worth sweeping.
Where used: Vault sweep scripts, balance checks
┌──────────────────────────────────────────────────────────────────┐
│ VAULT SWEEP FLOW │
│ │
│ Cron job checks all merchant/user vaults for leftover balances │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Vault A │ │ Vault B │ │
│ │ balance: $0.005 │ │ balance: $0.50 │ │
│ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ if (balance < MIN_SWEEP_AMOUNT) skip; │ │
│ │ MIN_SWEEP_AMOUNT = $0.01 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────┐ ┌──────────┐ │
│ │ SKIP │ │ SWEEP │ │
│ │ $0.005 │ │ $0.50 │ │
│ │ < $0.01 │ │ ≥ $0.01 │ │
│ └─────────┘ └────┬─────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Router/Treasury │ │
│ │ receives $0.50 │ │
│ └─────────────────┘ │
└──────────────────────────────────────────────────────────────────┘Why: Gas costs for on-chain transfers make sweeping sub-cent amounts economically wasteful. This threshold ensures sweeps are worthwhile.
MINIMUM_PROCESSABLE_AMOUNT (0.01 USDC)
What it controls: Transactions below 1 cent are skipped entirely.
Where used: shouldProcessAmount(), validateIncomingAmount(), calculateBridgeFee()
┌──────────────────────────────────────────────────────────────────┐
│ INCOMING TRANSACTION VALIDATION │
│ │
│ Webhook receives incoming USDC transfer │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Tx A │ │ Tx B │ │
│ │ amount: $0.009 │ │ amount: $50.00 │ │
│ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ truncateToCents(amount) │ │
│ │ $0.009 → $0.00 $50.00 → $50.00 │ │
│ └────────┬───────────────────────────┬────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ if (truncated < MINIMUM_PROCESSABLE_AMOUNT) skip; │ │
│ │ MINIMUM_PROCESSABLE_AMOUNT = $0.01 │ │
│ └────────┬───────────────────────────┬────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌────────────┐ ┌─────────────┐ │
│ │ SKIP │ │ PROCESS │ │
│ │ $0.00<$0.01│ │ $50≥$0.01 │ │
│ │ │ │ │ │
│ │ Return 200 │ │ Calculate │ │
│ │ {skipped} │ │ fees... │ │
│ └────────────┘ └─────────────┘ │
└──────────────────────────────────────────────────────────────────┘Why: Bridge API only processes whole cents. Amounts that truncate to less than 1 cent cannot be meaningfully processed and would result in 0% or negative payouts.
USDC 6-Decimal Precision
| USDC Amount | truncateToCents | roundFeeUp | Processable | |-------------|-----------------|------------|-------------| | 0.000001 | 0.00 | 0.01 | No | | 0.009999 | 0.00 | 0.01 | No | | 0.010000 | 0.01 | 0.01 | Yes | | 0.010001 | 0.01 | 0.02 | Yes | | 100.123456 | 100.12 | 100.13 | Yes |
SDK Structure
The SDK is organized into submodules that can be imported independently:
@psifi/sdk-node
├── /rounding - Bridge-compatible rounding and fee distribution
├── /constants - Threshold constants (DUST_THRESHOLD, MIN_SWEEP_AMOUNT)
├── /fees - Fee calculation service (requires DB models)
├── /entity - Entity resolution (requires DB models)
├── /checkout - V2 checkout session utilities and platform order transformations
└── /session - Secure checkout session management (requires Mongoose)Import Patterns
// Main entry point - all utilities
import {
calculateBridgeFee,
truncateToCents,
EntityResolver,
hasValidFeeConfig
} from '@psifi/sdk-node';
// Submodule imports (tree-shakeable)
import { calculateBridgeFee } from '@psifi/sdk-node/rounding';
import { DUST_THRESHOLD } from '@psifi/sdk-node/constants';
import { FeeService } from '@psifi/sdk-node/fees';
import { EntityResolver } from '@psifi/sdk-node/entity';
import { buildCheckoutMetadata, transformToShopifyOrder } from '@psifi/sdk-node/checkout';
import { SessionManager, registerCheckoutSessionModel } from '@psifi/sdk-node/session';Stateless vs Stateful Modules
| Module | DB Required | Use Case |
|--------|-------------|----------|
| /rounding | No | Pure math - rounding, fee calculation |
| /constants | No | Threshold values |
| /fees | Yes | Fee lookup with custom merchant/user fees |
| /entity | Yes | Resolve PsiTag/address/vaultId to entity |
| /checkout | No | V2 session metadata, platform order transformations |
| /session | Yes (Mongoose) | Secure checkout session management |
Entity Resolution
The EntityResolver is the single source of truth for resolving any identifier to a user, merchant, or external entity.
Supported Identifier Types
| Type | Pattern | Example |
|------|---------|---------|
| PsiTag | 3-15 alphanumeric + underscore | peptideempires |
| Solana Address | 32-44 base58 chars | 7xKXtg2CW87d97... |
| Ethereum Address | 0x + 40 hex chars | 0x742d35Cc6634... |
| MongoDB ID | 24 hex chars | 507f1f77bcf86cd799439011 |
| Clerk ID | user_ or org_ prefix | user_2NNEq... |
| Wallet ID | UUID format | 550e8400-e29b-41d4-a716-446655440000 |
| Vault ID | 1-6 digits | 12345 |
Usage
import { createEntityResolver } from '@psifi/sdk-node/entity';
// Initialize with your Mongoose models
const resolver = createEntityResolver({
Merchant: require('./models/merchant.model.js'),
User: require('./models/user.model.js'),
});
// Resolve any identifier
const result = await resolver.resolve('peptideempires');
// {
// type: 'merchant',
// identifier: 'peptideempires',
// entity: { /* full Mongoose document */ },
// vaultId: '12345',
// walletId: '550e8400-e29b-...',
// accountId: '0',
// psiTag: 'peptideempires',
// name: 'Peptide Empires',
// resolvedBy: 'psiTag'
// }
// Resolve by blockchain address
const user = await resolver.resolve('7xKXtg2CW87d97TZxPFp...');
// { type: 'user', entity: {...}, ... }
// External addresses return type: 'external'
const external = await resolver.resolve('UnknownSolanaAddress123...');
// { type: 'external', vaultId: null, walletId: null, ... }Resolution Priority
When an identifier matches multiple patterns (e.g., numeric string could be PsiTag or vault ID), the resolver tries in this order:
- PsiTag lookup first
- If not found and numeric, try vault ID
- Return external if not found
Fee Service
The FeeService provides unified fee calculation with database access for custom merchant/user fee configurations.
Initialization
import { FeeService, createFeeService } from '@psifi/sdk-node/fees';
// Option 1: Factory function
const feeService = createFeeService({
Merchant: MerchantModel,
User: UserModel,
Fee: FeeModel,
});
// Option 2: Class instantiation
const feeService = new FeeService({
models: { Merchant, User, Fee }
});Calculate Transfer Fees
// Calculate fee for any transfer (resolves entities automatically)
const result = await feeService.calculateFee({
source: 'external-solana-address',
destination: 'peptideempires', // PsiTag, address, or ID
amount: 200,
assetId: 'USDC',
});
// {
// success: true,
// feeAmount: 2.00,
// feePercentage: 1,
// effectiveFeePercentage: 1,
// originalAmount: 200,
// netAmount: 198,
// feeSource: 'custom-receiving-merchant-peptideempires',
// sourceType: 'external',
// destinationType: 'merchant',
// isoFeeBreakdown: null // or ISO split details
// }Specialized Fee Methods
// Bridge on-ramp fee (fiat deposits)
const bridgeFee = await feeService.calculateBridgeOnrampFee({
merchantId: '507f1f77bcf86cd799439011',
amount: 1000,
});
// Bridge withdrawal fee
const withdrawalFee = await feeService.calculateBridgeWithdrawalFee({
psiTag: 'peptideempires',
amount: 500,
});
// Card load fee
const cardFee = await feeService.calculateCardLoadFee({
psiTag: 'johndoe',
amount: 100,
});Fee Priority (Custom vs Global)
Fees are resolved in priority order:
- Custom fees -
entity.customFees.{direction}.{type}(if enabled) - Legacy merchant fee -
merchant.feePercentage(deprecated, still supported) - ISO sub-merchant fees - Split fees for ISO partners
- Global fees -
Feecollection withisDefault: true
Fee Utilities (Stateless)
For simple fee calculations without database access:
import { hasValidFeeConfig, applyFeeStructure } from '@psifi/sdk-node';
// Check if a fee config is valid (not empty Mongoose object)
hasValidFeeConfig({}); // false
hasValidFeeConfig({ percentage: 5 }); // true
hasValidFeeConfig({ percentage: 0 }); // true (0% is valid)
// Apply fee structure with Bridge-compatible rounding
const result = applyFeeStructure(200, {
percentage: 1.5,
flatFeeCents: 25,
minimumCents: 50,
maximumCents: 500,
});
// { feeAmount: 3.25, netAmount: 196.75, ... }Checkout Utilities
The /checkout module provides utilities for V2 secure checkout sessions and e-commerce platform order transformations.
Session Metadata Builder
Build standardized metadata for checkout sessions:
import { buildCheckoutMetadata, isV2Session, V2_SESSION_PREFIX } from '@psifi/sdk-node/checkout';
// Check if a session ID is V2 format
isV2Session('v2_abc123'); // true
isV2Session('old_session'); // false
// Build metadata from webhook/session data
const metadata = buildCheckoutMetadata({
sessionId: 'v2_abc123',
provider: 'banxa',
amount: 100.00,
currency: 'USD',
cryptoCurrency: 'USDC',
customerEmail: '[email protected]',
customerFirstName: 'John',
customerLastName: 'Doe',
items: [
{ name: 'Widget', quantity: 2, price: 50.00, sku: 'WGT-001' }
],
shippingAddress: {
address1: '123 Main St',
city: 'Austin',
state: 'TX',
zip: '78701',
country: 'US'
},
platformOrderId: 'shop_12345',
platform: 'shopify'
});Session Builder
Build session data for creating checkout sessions:
import { buildSessionData, buildSessionItems, validateSessionData } from '@psifi/sdk-node/checkout';
// Build line items
const items = buildSessionItems([
{ name: 'Product A', price: 29.99, quantity: 2 },
{ name: 'Product B', price: 49.99, quantity: 1 }
]);
// Build full session data
const sessionData = buildSessionData({
merchantPsiTag: 'mystore',
amount: 109.97,
currency: 'USD',
items,
customerEmail: '[email protected]',
successUrl: 'https://mystore.com/success',
cancelUrl: 'https://mystore.com/cancel',
});
// Validate before sending
const validation = validateSessionData(sessionData);
if (!validation.isValid) {
console.error('Validation errors:', validation.errors);
}Platform Order Transformations
Transform checkout metadata into platform-specific order formats:
import {
transformToShopifyOrder,
transformToWooCommerceOrder,
metadataToShopifyOrder,
detectOrderPlatforms
} from '@psifi/sdk-node/checkout';
// Detect which platforms an order belongs to
const platforms = detectOrderPlatforms(metadata);
// { shopify: true, woocommerce: false }
// Transform to Shopify order format
const shopifyOrder = transformToShopifyOrder(metadata, {
financialStatus: 'paid',
fulfillmentStatus: 'unfulfilled',
tags: ['crypto-payment', 'psifi']
});
// Transform to WooCommerce order format
const wooOrder = transformToWooCommerceOrder(metadata, {
status: 'processing',
paymentMethod: 'crypto_usdc'
});
// One-liner from raw metadata
const order = metadataToShopifyOrder(rawCheckoutMetadata);Exported Constants
import {
V2_SESSION_PREFIX, // 'v2_'
REQUIRED_METADATA_FIELDS, // Fields required for metadata
OPTIONAL_METADATA_FIELDS, // Optional metadata fields
} from '@psifi/sdk-node/checkout';Session Management
The /session module provides secure checkout session management with AES-256-GCM encryption, similar to Stripe's Checkout Sessions.
Quick Start
import mongoose from 'mongoose';
import { SessionManager, registerCheckoutSessionModel } from '@psifi/sdk-node/session';
// 1. Register the model with your mongoose instance
const CheckoutSession = registerCheckoutSessionModel(mongoose);
// 2. Create a SessionManager instance
const sessionManager = new SessionManager({
CheckoutSession,
encryptionSecret: process.env.CHECKOUT_ENCRYPTION_SECRET, // min 32 chars
Merchant: MerchantModel, // optional - for merchant validation
isTestEnvironment: false,
});
// 3. Create a session
const session = await sessionManager.createSession({
merchantId: 'merchant_123',
items: [
{ name: 'Product A', price: 29.99, quantity: 2 },
{ name: 'Product B', price: 49.99, quantity: 1 },
],
customerEmail: '[email protected]',
paymentMethod: 'banxa',
}, {
ipAddress: req.ip,
userAgent: req.headers['user-agent'],
});
console.log(session.sessionId); // cs_secure_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
// 4. Validate and retrieve a session
const { sessionData, checkoutData } = await sessionManager.validateSession(
session.sessionId,
{ ipAddress: req.ip, userAgent: req.headers['user-agent'] }
);Session Modes
Payment Mode (default) - Pre-selected items:
await sessionManager.createSession({
merchantId: '...',
mode: 'payment',
items: [{ name: 'Widget', price: 99.99, quantity: 1 }],
paymentMethod: 'banxa',
}, clientInfo);Cart Mode - Customer selects from available products:
await sessionManager.createSession({
merchantId: '...',
mode: 'cart',
availableProducts: [
{ productId: 'prod_1', name: 'Widget A', price: 29.99 },
{ productId: 'prod_2', name: 'Widget B', price: 49.99 },
],
pricingStrategy: 'PER_ITEM',
}, clientInfo);Encryption Utilities (Stateless)
Use encryption functions directly without SessionManager:
import {
encryptCheckoutData,
decryptCheckoutData,
generateSessionId,
generateSignature,
verifySignature,
} from '@psifi/sdk-node/session';
const sessionId = generateSessionId();
// cs_secure_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
const encrypted = encryptCheckoutData(
{ items: [...], amount: 100 },
sessionId,
encryptionSecret
);
// { encrypted: '...', iv: '...', tag: '...' }
const data = decryptCheckoutData(encrypted, sessionId, encryptionSecret);Validation Utilities
import {
validateCheckoutData,
isV2Session,
isValidEmail,
extractCartSummary,
hasMeaningfulState,
} from '@psifi/sdk-node/session';
// Check session ID format
isV2Session('cs_secure_xxx'); // true
isV2Session('legacy_session'); // false
// Validate checkout data (throws on error)
validateCheckoutData({
merchantId: '...',
items: [...],
});
// Extract cart summary for analytics
const summary = extractCartSummary({ items: [...] });
// { itemCount: 3, items: [...], subtotal: 129.97, discount: null }Security Model
- Cryptographic Session IDs - UUIDv4 with
cs_secure_prefix - AES-256-GCM Encryption - All checkout data encrypted at rest
- HMAC-SHA256 Signatures - Tamper detection
- Time-Limited - 30 minute default expiration
- Single-Use - Sessions marked as used after payment
For full documentation, see psifi-docs/docs/sdk/session.md.
Edge Case Handling
All functions safely handle invalid inputs:
truncateToCents(null) // 0
truncateToCents(undefined) // 0
truncateToCents(NaN) // 0
truncateToCents(Infinity) // 0
truncateToCents(-100) // 0
truncateToCents('abc') // 0
truncateToCents('100.50') // 100.50 (strings parsed)Import Styles
// Full import (recommended)
import { calculateBridgeFee, roundFeeUp } from '@psifi/sdk-node';
// Submodule import
import { calculateBridgeFee } from '@psifi/sdk-node/rounding';Testing
The SDK includes 400+ comprehensive tests:
# Run all tests
pnpm test
# Run specific test suites
node src/rounding/bridgeRounding.test.js # 228 tests
node src/rounding/feeDistribution.test.js # 37 tests
node src/session/sessionEncryption.test.js # 36 tests
node src/session/sessionValidation.test.js # 52 testsTest coverage includes:
- All Bridge API documentation examples
- USDC 6-decimal precision (micro-amounts to 0.000001)
- Edge cases: negative, zero, null, undefined, NaN, Infinity
- JavaScript floating point precision issues
- ISO fee distribution scenarios
- N-way fee distribution with priority-based capping
- Session encryption/decryption and signature verification
- Random value invariant tests (400+ iterations total)
Version History
| Version | Changes |
|---------|---------|
| 1.3.2 | Fixed Mongoose type reserved keyword in schema, changed expiresAt from TTL index to regular index (sessions preserved, not auto-deleted) |
| 1.3.1 | Fixed merchant active status check to support both active: boolean and status: 'ACTIVE' patterns |
| 1.3.0 | Added /session module: SessionManager, AES-256-GCM encryption, HMAC signatures, Mongoose schema, abandoned cart tracking |
| 1.2.0 | Added /checkout module: V2 session metadata utilities, session builder, platform order transformations (Shopify, WooCommerce) |
| 1.1.0 | Set DUST_THRESHOLD to 0 (no dust reserve needed with Bridge rounding), cleaned up unused constants |
| 1.0.5 | Added n-way fee distribution (calculateFeeDistribution) with priority-based capping - fixes over-rounding issues |
| 1.0.4 | Removed fee percentage constants (fees from API only), removed MICRO_UNIT_THRESHOLD, disabled DUST_THRESHOLD (set to 0), added precision philosophy docs |
| 1.0.3 | Added operational thresholds: DUST_THRESHOLD, MIN_SWEEP_AMOUNT |
| 1.0.2 | Infinity handling, 228 core tests, 59 ISO tests |
| 1.0.1 | Epsilon fix for floating point precision |
| 1.0.0 | Initial release |
Documentation
- Full Documentation (internal)
- Bridge API Precision Docs
License
MIT
