npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@psifi/sdk-node

v1.112.18

Published

PsiFi Node.js SDK - Shared utilities for PsiFi services

Readme

@psifi/sdk-node

PsiFi Node.js SDK - Shared utilities for all PsiFi backend services.

npm version

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.37

Why 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.01

roundFeeUp(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.00

calculateBridgeFee(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.11

N-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 amount
  • feeRecipients - 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 metadata
  • totalFees, 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.10

Flow 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 error

Constants

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:

  1. PsiTag lookup first
  2. If not found and numeric, try vault ID
  3. 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:

  1. Custom fees - entity.customFees.{direction}.{type} (if enabled)
  2. Legacy merchant fee - merchant.feePercentage (deprecated, still supported)
  3. ISO sub-merchant fees - Split fees for ISO partners
  4. Global fees - Fee collection with isDefault: 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 tests

Test 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

License

MIT