Platform-agnostic SDK for PERS (Phygital Experience Rewards System) - Core business logic and API integration
npm install @explorins/pers-sdk@explorins/pers-sdk:
bash
npm install @explorins/pers-shared ethers@^6.15.0
`
| Package | Version | Purpose |
|---------|---------|---------|
| @explorins/pers-shared | * | Shared types, interfaces, and DTOs used across all SDK domains |
| ethers | ^6.15.0 | Blockchain/Web3 operations (required for sdk.web3 manager) |
$3
| Package | Required For | Notes |
|---------|--------------|-------|
| rxjs | AngularHttpClientAdapter only | Loaded dynamically at runtime; NOT bundled into SDK |
> AI Agent Note: The SDK has ZERO runtime RxJS dependency unless you specifically use AngularHttpClientAdapter. Browser, Node.js, and React Native integrations do NOT require RxJS.
$3
The SDK bundles essential dependencies for seamless installation:
| Package | Type | Purpose |
|---------|------|---------|
| @explorins/pers-shared | Dependency (bundled) | Shared types, interfaces, and DTOs |
| @explorins/web3-ts | Dependency (bundled) | Web3/blockchain utilities - automatically tree-shaken when not used |
| ethers | Dependency (bundled) | Blockchain operations - automatically tree-shaken when not used |
> Tree-Shaking Magic: Web3 features (~500KB install size) are only included in your final bundle if you actually use them. The SDK uses subpath exports (@explorins/pers-sdk/web3) that modern bundlers (Vite, Webpack 5+, Angular) automatically tree-shake when unused.
>
> Note: npm install downloads ~500KB of web3 dependencies, but your production bundle will be ~3KB if you don't import web3 features. This design prioritizes developer experience (no peer dependency hassles) while maintaining optimal production bundle sizes through automatic tree-shaking.
---
Installation
`bash
Single command installs everything - no peer dependency hassle
npm install @explorins/pers-sdk
Optional: For Angular applications only
npm install rxjs
`
Bundle Sizes (After Tree-Shaking):
| Usage Pattern | Install Size | Runtime Bundle Size | Notes |
|--------------|--------------|---------------------|-------|
| Core SDK only (no web3) | ~500 KB | ~3 KB (ESM) | Campaigns, users, tokens, auth |
| With Web3 features | ~500 KB | ~26 KB (ESM) | Includes blockchain operations |
| Development (node_modules) | ~500 KB | N/A | All features available for development |
> Key Insight: The install downloads 500KB, but your production app only includes what you import. If you never import { Web3Manager }, those 500KB are automatically excluded from your bundle by the bundler.
How Tree-Shaking Works:
`typescript
// ✅ This code = ~3 KB production bundle (web3 dependencies NOT included)
import { createPersSDK } from '@explorins/pers-sdk';
const sdk = createPersSDK({...});
await sdk.campaign.list();
// ✅ This code = ~26 KB production bundle (web3 loaded only when imported)
import { Web3Manager } from '@explorins/pers-sdk/web3';
const web3 = new Web3Manager(sdk.api());
await web3.getTokenBalance({...});
`
Modern bundlers (Vite, Webpack 5+, Angular, Rollup) detect when web3 code is never imported and completely remove it from your production bundle through dead code elimination.
---
Quick Start
$3
`typescript
import { createPersSDK } from '@explorins/pers-sdk';
import { BrowserFetchClientAdapter } from '@explorins/pers-sdk/platform-adapters';
// Initialize SDK
const sdk = createPersSDK(new BrowserFetchClientAdapter(), {
apiProjectKey: 'your-project-key'
});
// Authenticate with external JWT (Firebase, Auth0, Cognito, etc.)
const externalJWT = await yourAuthProvider.getIdToken();
const authResult = await sdk.auth.loginWithToken(externalJWT, 'user');
// Use SDK managers
const campaigns = await sdk.campaigns.getActiveCampaigns();
`
$3
`typescript
import { createNodeSDK } from '@explorins/pers-sdk/node';
// One-liner setup with static JWT
const sdk = createNodeSDK({
jwt: 'your-system-jwt',
projectKey: 'your-project-key',
environment: 'production'
});
// Ready to use - no additional auth needed
const campaigns = await sdk.campaigns.getCampaigns();
const userClaims = await sdk.campaigns.getCampaignClaims({ userId: 'user-123' });
`
$3
`typescript
import { createNodeSDK } from '@explorins/pers-sdk/node';
// Initialize with static JWT (tenant/business system token)
const sdk = createNodeSDK({
jwt: process.env.PERS_JWT_TOKEN,
projectKey: process.env.PERS_PROJECT_KEY,
environment: 'production'
});
// Campaign operations (admin/system access)
const campaigns = await sdk.campaigns.getCampaigns();
const campaign = await sdk.campaigns.getCampaignById('campaign-123');
// Claim campaign for a user (system operation)
const claim = await sdk.campaigns.claimCampaign({
campaignId: 'campaign-123',
userIdentifier: 'external-user-id'
});
// Get user's claim history
const userId = claim.user?.id;
const userClaims = await sdk.campaigns.getCampaignClaims({ userId });
// Business operations
const businesses = await sdk.businesses.getActiveBusinesses();
`
---
Platform Integration
$3
No additional dependencies required.
`typescript
import { createPersSDK } from '@explorins/pers-sdk';
import { BrowserFetchClientAdapter } from '@explorins/pers-sdk/platform-adapters';
const sdk = createPersSDK(new BrowserFetchClientAdapter(), {
environment: 'production',
apiProjectKey: 'your-project-key'
});
// Ready to use
const campaigns = await sdk.campaigns.getActiveCampaigns();
`
> ⚠️ Browser Note: Always use BrowserFetchClientAdapter for browser/React/Vue apps. Do not import NodeHttpClientAdapter in browser code - it contains Node.js-specific imports that will cause build errors.
$3
Requires rxjs peer dependency.
`bash
npm install rxjs
`
`typescript
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { createPersSDK, PersSDK } from '@explorins/pers-sdk';
import { AngularHttpClientAdapter } from '@explorins/pers-sdk/platform-adapters';
import { IndexedDBTokenStorage } from '@explorins/pers-sdk/core';
@Injectable({ providedIn: 'root' })
export class PersSDKService {
private readonly sdk: PersSDK;
constructor() {
const httpClient = inject(HttpClient);
// Use createPersSDK factory or new PersSDK()
this.sdk = createPersSDK(
new AngularHttpClientAdapter(httpClient),
{
environment: 'production',
apiProjectKey: 'your-project-key',
authStorage: new IndexedDBTokenStorage() // Recommended
}
);
}
// Expose managers directly for clean component access
get auth() { return this.sdk.auth; }
get users() { return this.sdk.users; }
get tokens() { return this.sdk.tokens; }
get businesses() { return this.sdk.businesses; }
get campaigns() { return this.sdk.campaigns; }
get redemptions() { return this.sdk.redemptions; }
get transactions() { return this.sdk.transactions; }
get purchases() { return this.sdk.purchases; }
get web3() { return this.sdk.web3; }
// ... other managers as needed
}
`
$3
No additional dependencies required. Use the dedicated Node.js entry point:
`typescript
import { createNodeSDK } from '@explorins/pers-sdk/node';
// Convenience function for Node.js - auto-configures adapter
const sdk = createNodeSDK({
jwt: 'your-jwt-token',
projectKey: 'your-project-key',
environment: 'production'
});
// Ready to use
const campaigns = await sdk.campaigns.getActiveCampaigns();
const userClaims = await sdk.campaigns.getCampaignClaims({
userId: 'user-123'
});
`
$3
Use the dedicated React Native SDK (includes passkeys, secure storage, DPoP):
`bash
npm install @explorins/pers-sdk-react-native @react-native-async-storage/async-storage
`
`typescript
// In your root layout (e.g., _layout.tsx for Expo Router)
import { PersSDKProvider } from '@explorins/pers-sdk-react-native';
export default function RootLayout() {
return (
);
}
// In your components - use hooks
import { useAuth, useTokens, useCampaigns } from '@explorins/pers-sdk-react-native';
function MyComponent() {
const { login, isAuthenticated } = useAuth();
const { getTokens } = useTokens();
const { getActiveCampaigns, claimCampaign } = useCampaigns();
// ...
}
`
See @explorins/pers-sdk-react-native for full setup including passkey configuration.
---
Architecture
The SDK uses a clean Manager-Service pattern with three access levels:
`typescript
// ═══════════════════════════════════════════════════════════════════════════
// MANAGER LAYER (Recommended - High-level, intuitive APIs)
// ═══════════════════════════════════════════════════════════════════════════
sdk.auth // Authentication & sessions
sdk.users // User profiles & management
sdk.userStatus // User status/tier management
sdk.tokens // Token types & configuration
sdk.businesses // Business operations
sdk.campaigns // Marketing campaigns
sdk.redemptions // Reward redemptions
sdk.transactions // Transaction history
sdk.purchases // Purchase/payment processing
sdk.analytics // Reporting & insights
sdk.tenants // Multi-tenant configuration
sdk.donations // Charitable giving
sdk.files // File upload/download operations
sdk.web3 // Blockchain operations
sdk.apiKeys // API key management (Admin)
// ═══════════════════════════════════════════════════════════════════════════
// SERVICE LAYER (Advanced - Full domain access)
// ═══════════════════════════════════════════════════════════════════════════
sdk.campaigns.getCampaignService() // Full CampaignService access
sdk.tokens.getTokenService() // Full TokenService access
sdk.businesses.getBusinessService() // Full BusinessService access
sdk.purchases.getPurchaseService() // Full PaymentService access
// ... each manager exposes its underlying service
// ═══════════════════════════════════════════════════════════════════════════
// API LAYER (Expert - Direct REST API access)
// ═══════════════════════════════════════════════════════════════════════════
const apiClient = sdk.api();
const customData = await apiClient.get('/custom-endpoint');
await apiClient.post('/custom-endpoint', { data: 'value' });
`
$3
| Manager | Accessor | Primary Use Cases |
|---------|----------|-------------------|
| AuthManager | sdk.auth | Login, logout, token management, authentication status |
| UserManager | sdk.users | User profiles, account management |
| UserStatusManager | sdk.userStatus | User tiers, status levels |
| TokenManager | sdk.tokens | Token types, credit tokens, reward tokens |
| BusinessManager | sdk.businesses | Business discovery, details, types |
| CampaignManager | sdk.campaigns | Campaign discovery, claiming, user history |
| RedemptionManager | sdk.redemptions | Redeem rewards, redemption history |
| TransactionManager | sdk.transactions | Transaction history, details |
| PurchaseManager | sdk.purchases | Payment intents, purchase tokens |
| AnalyticsManager | sdk.analytics | Reporting, transaction analytics |
| TenantManager | sdk.tenants | Tenant config, client settings |
| DonationManager | sdk.donations | Donation types, charitable giving |
| FileManager | sdk.files | Signed URLs, media optimization |
| Web3Manager | sdk.web3 | Blockchain operations, token metadata |
| ApiKeyManager | sdk.apiKeys | API key CRUD (Admin only) |
---
Core Features
$3
The SDK provides a platform-agnostic event system for subscribing to SDK-wide events. All events have a userMessage field ready for UI display.
`typescript
// Subscribe to ALL events - one handler
const unsubscribe = sdk.events.subscribe((event) => {
showNotification(event.userMessage, event.level);
});
// Filter by domain and level
sdk.events.subscribe((event) => {
logToSentry(event);
}, { level: 'error' });
// Only transaction successes
sdk.events.subscribe((event) => {
playSuccessSound();
confetti();
}, { domain: 'transaction', level: 'success' });
// One-time event (auto-unsubscribe)
sdk.events.once((event) => {
console.log('First event:', event.type);
});
// Cleanup
unsubscribe();
`
#### Event Domains
| Domain | Manager | Events |
|--------|---------|--------|
| authentication | sdk.auth | LOGIN_SUCCESS |
| user | sdk.users | PROFILE_UPDATED |
| business | sdk.businesses | BUSINESS_CREATED, BUSINESS_UPDATED, MEMBERSHIP_UPDATED |
| campaign | sdk.campaigns | CAMPAIGN_CLAIMED |
| redemption | sdk.redemptions | REDEMPTION_COMPLETED |
| transaction | sdk.transactions | TRANSACTION_CREATED, TRANSACTION_SUBMITTED |
#### Event Types
`typescript
import type {
PersEvent, // Base event type (discriminated union)
SuccessEvent, // Success events (business domains)
ErrorEvent, // Error events (all domains including technical)
EventFilter, // Filter for subscriptions
EventHandler // Handler type (sync or async)
} from '@explorins/pers-sdk/core';
// Event structure
interface PersEvent {
id: string; // Unique event ID
timestamp: number; // Unix timestamp (ms)
domain: string; // Event domain
type: string; // Event type within domain
level: 'success' | 'error';
userMessage: string; // Ready for UI display
action?: string; // Suggested action
code?: string; // Backend error code
details?: object; // Additional data for logging
}
`
#### Async Handler Support
Event handlers can be synchronous or asynchronous. Async errors are caught automatically:
`typescript
sdk.events.subscribe(async (event) => {
await saveToDatabase(event); // Async operations supported
await sendAnalytics(event);
});
`
---
$3
The SDK uses external JWT tokens for authentication. You provide a JWT from your authentication provider (Firebase, Auth0, Cognito, etc.), and the SDK exchanges it for PERS access tokens.
`typescript
// Step 1: Get JWT from your auth provider (Firebase example)
import { getAuth } from 'firebase/auth';
const firebaseUser = getAuth().currentUser;
const externalJWT = await firebaseUser?.getIdToken();
// Step 2: Exchange JWT for PERS tokens
const authResult = await sdk.auth.loginWithToken(externalJWT, 'user');
console.log('User authenticated:', authResult.user.name);
// Admin login
const adminResult = await sdk.auth.loginWithToken(adminJWT, 'admin');
console.log('Admin authenticated:', adminResult.admin.email);
// Check authentication status (async - verifies with server)
const isAuth = await sdk.auth.isAuthenticated();
// Quick check if tokens exist locally (faster, less reliable)
const hasTokens = await sdk.auth.hasValidAuth();
// Get current user
const user = await sdk.auth.getCurrentUser();
// Logout / clear auth
await sdk.auth.clearAuth();
`
How it works:
1. Your app authenticates users via Firebase/Auth0/etc.
2. You call sdk.auth.loginWithToken(jwt) with that JWT
3. PERS validates the JWT and returns PERS-specific access/refresh tokens
4. SDK stores tokens and handles automatic refresh
5. All subsequent SDK calls are automatically authenticated
Security Features:
- DPoP (Demonstrating Proof-of-Possession): Enabled by default - binds tokens to client
- Automatic token refresh and validation
- Flexible storage strategies (LocalStorage, IndexedDB, Memory)
- Support for user and admin authentication flows
$3
`typescript
// Get all active businesses
const businesses = await sdk.businesses.getActiveBusinesses();
// Get business by ID
const business = await sdk.businesses.getBusinessById('business-123');
// Get business types
const types = await sdk.businesses.getBusinessTypes();
`
$3
`typescript
// Get active campaigns available for claiming
const campaigns = await sdk.campaigns.getActiveCampaigns();
// Get campaign details
const campaign = await sdk.campaigns.getCampaignById('campaign-123');
// Claim campaign rewards
const claim = await sdk.campaigns.claimCampaign({
campaignId: 'campaign-123',
businessId: 'business-456' // Optional: associated business
});
// Get user's campaign claim history
const userClaims = await sdk.campaigns.getUserClaims();
`
$3
`typescript
// Get all token types
const tokens = await sdk.tokens.getTokens();
// Get active credit token (main loyalty currency)
const creditToken = await sdk.tokens.getActiveCreditToken();
// Get reward tokens
const rewards = await sdk.tokens.getRewardTokens();
// Get status tokens (tier/achievement tokens)
const statusTokens = await sdk.tokens.getStatusTokens();
// Get token by blockchain contract
const token = await sdk.tokens.getTokenByContract('0x123...', 'token-id');
`
$3
> Important Architecture Note: The PERS backend stores token definitions (contract addresses, ABIs, metadata), but does NOT store user token balances. User balances are queried directly from the blockchain via RPC calls using sdk.web3.* methods.
Data Flow:
1. sdk.tokens.* → Get token definitions (contract address, ABI, chainId) from PERS backend
2. sdk.auth.getCurrentUser() → Get user's wallet address
3. sdk.web3.* → Query blockchain directly for user's token balance
#### Points Balance (ERC-20)
`typescript
// Step 1: Get the credit token definition from PERS backend
const creditToken = await sdk.tokens.getActiveCreditToken();
// Step 2: Get user's wallet address
const user = await sdk.auth.getCurrentUser();
const walletAddress = user.wallets?.[0]?.address;
// Step 3: Query blockchain directly for balance
const balance = await sdk.web3.getTokenBalance({
accountAddress: walletAddress,
contractAddress: creditToken.contractAddress,
abi: creditToken.abi, // Raw ABI from backend works directly
chainId: creditToken.chainId,
tokenId: '' // Empty for ERC-20
});
console.log('Points balance:', balance.balance);
console.log('Has balance:', balance.hasBalance);
`
#### Stamps Collection (ERC-721 / ERC-1155)
`typescript
// Step 1: Get status token definitions (stamps/achievements)
const statusTokens = await sdk.tokens.getStatusTokens();
// Step 2: Get user's wallet address
const user = await sdk.auth.getCurrentUser();
const walletAddress = user.wallets?.[0]?.address;
// Step 3: Query blockchain for user's stamp collection
for (const stampToken of statusTokens) {
const collection = await sdk.web3.getTokenCollection({
accountAddress: walletAddress,
contractAddress: stampToken.contractAddress,
abi: stampToken.abi, // Raw ABI from backend works directly
chainId: stampToken.chainId,
maxTokens: 50 // Optional: limit results
});
// Filter to tokens with balance > 0
const ownedStamps = collection.tokens.filter(t => t.hasBalance && t.balance > 0);
console.log(Stamps from ${stampToken.symbol}:, ownedStamps.length);
// Each stamp includes metadata (name, description, image, etc.)
for (const stamp of ownedStamps) {
console.log(- Token #${stamp.tokenId}: ${stamp.metadata?.name});
console.log( Image: ${stamp.metadata?.imageUrl});
}
}
`
#### Complete Example: Load All User Balances
`typescript
async function loadUserTokenBalances(sdk: PersSDK) {
// Get user wallet
const user = await sdk.auth.getCurrentUser();
const walletAddress = user.wallets?.[0]?.address;
if (!walletAddress) throw new Error('User has no wallet');
// Get all token definitions
const [creditToken, statusTokens, rewardTokens] = await Promise.all([
sdk.tokens.getActiveCreditToken(),
sdk.tokens.getStatusTokens(),
sdk.tokens.getRewardTokens()
]);
// Query ERC-20 balance (Points)
const pointsBalance = creditToken ? await sdk.web3.getTokenBalance({
accountAddress: walletAddress,
contractAddress: creditToken.contractAddress,
abi: creditToken.abi,
chainId: creditToken.chainId,
tokenId: ''
}) : null;
// Query ERC-721/ERC-1155 collections (Stamps, Rewards)
const allNftTokens = [...(statusTokens || []), ...(rewardTokens || [])];
const collections = await Promise.all(
allNftTokens.map(token =>
sdk.web3.getTokenCollection({
accountAddress: walletAddress,
contractAddress: token.contractAddress,
abi: token.abi,
chainId: token.chainId,
maxTokens: 100
}).catch(() => null) // Handle individual failures gracefully
)
);
return {
points: pointsBalance,
stamps: collections.filter(Boolean)
};
}
`
$3
The SDK provides factory functions for building transaction request DTOs. These are tree-shakeable and provide type-safe transaction creation:
`typescript
import {
buildMintRequest,
buildBurnRequest,
buildTransferRequest,
buildPOSTransferRequest,
buildSubmissionRequest
} from '@explorins/pers-sdk/transaction';
import { AccountOwnerType } from '@explorins/pers-shared';
// Mint tokens to a recipient
const mintRequest = buildMintRequest({
amount: 100,
contractAddress: '0x...',
chainId: 137,
recipientAccountId: 'user-123',
recipientAccountType: AccountOwnerType.USER
});
// Burn tokens
const burnRequest = buildBurnRequest({
amount: 50,
contractAddress: '0x...',
chainId: 137,
senderAccountId: 'user-123',
senderAccountType: AccountOwnerType.USER
});
// Transfer tokens between accounts
const transferRequest = buildTransferRequest({
amount: 100,
contractAddress: '0x...',
chainId: 137,
senderAccountId: 'user-123',
senderAccountType: AccountOwnerType.USER,
recipientAccountId: 'business-456',
recipientAccountType: AccountOwnerType.BUSINESS
});
// Submit the transaction
const result = await sdk.transactions.createTransaction(mintRequest);
`
#### POS Transaction Flow
For Point-of-Sale scenarios where a business submits a transaction on behalf of a user, use buildPOSTransferRequest:
`typescript
import { buildPOSTransferRequest } from '@explorins/pers-sdk/transaction';
// POS flow: User pays business, business authorized to submit
const posRequest = buildPOSTransferRequest({
amount: 100,
contractAddress: '0x...',
chainId: 137,
userId: 'user-123', // User sending tokens
businessId: 'business-456' // Business receiving & authorized to submit
});
// This automatically sets:
// - engagedBusinessId: 'business-456' (for reporting)
// - authorizedSubmitterId: 'business-456' (can submit the signed tx)
// - authorizedSubmitterType: AccountOwnerType.BUSINESS
const result = await sdk.transactions.createTransaction(posRequest);
`
For custom authorization scenarios, use buildTransferRequest with manual POS fields:
`typescript
import { buildTransferRequest, type POSAuthorizationOptions } from '@explorins/pers-sdk/transaction';
const customPOSRequest = buildTransferRequest({
amount: 100,
contractAddress: '0x...',
chainId: 137,
senderAccountId: 'user-123',
senderAccountType: AccountOwnerType.USER,
recipientAccountId: 'business-456',
recipientAccountType: AccountOwnerType.BUSINESS,
// POS authorization fields
engagedBusinessId: 'business-456',
authorizedSubmitterId: 'business-456',
authorizedSubmitterType: AccountOwnerType.BUSINESS
});
`
| POS Field | Type | Description |
|-----------|------|-------------|
| engagedBusinessId | string | Business commercially involved (for stats/reporting) |
| authorizedSubmitterId | string | Entity authorized to submit the signed transaction |
| authorizedSubmitterType | AccountOwnerType | Type of authorized submitter (USER or BUSINESS) |
`
$3
`typescript
// Create payment intent
const intent = await sdk.purchases.createPaymentIntent(
100, // amount
'usd', // currency
'user@example.com', // email
'Token Purchase' // description
);
// Get user's purchase history
const purchases = await sdk.purchases.getAllUserPurchases();
// Get available purchase tokens
const purchaseTokens = await sdk.purchases.getActivePurchaseTokens();
`
---
Configuration
$3
The apiProjectKey is a tenant-specific identifier that associates your application with a PERS tenant (organization). You must obtain this from the PERS admin dashboard or from your PERS account manager.
`typescript
// Example project key format (64-character hex string)
apiProjectKey: 'e3e16b5863f0a042b949650d236a37b0758bd51177463d627921112d2291fe01'
`
$3
`typescript
import { PersSDK } from '@explorins/pers-sdk';
import { IndexedDBTokenStorage } from '@explorins/pers-sdk/core';
import { BrowserFetchClientAdapter } from '@explorins/pers-sdk/platform-adapters';
const sdk = new PersSDK(new BrowserFetchClientAdapter(), {
// Environment: 'development' | 'staging' | 'production'
environment: 'production', // Default: 'production'
// Project key for API authentication (required)
apiProjectKey: 'your-project-key',
// API version (currently only v2 supported)
apiVersion: 'v2', // Default: 'v2'
// Request timeout in milliseconds
timeout: 30000, // Default: 30000
// Retry attempts for failed requests
retries: 3, // Default: 3
// Token storage strategy (recommended: IndexedDB)
authStorage: new IndexedDBTokenStorage(), // Default: LocalStorage
// DPoP (Demonstrating Proof-of-Possession) configuration
dpop: {
enabled: true, // Default: true (recommended for security)
},
// Authentication type for auto-created provider
authType: 'user', // 'user' | 'admin', Default: 'user'
// Token refresh margin (seconds before expiry to refresh)
tokenRefreshMargin: 60, // Default: 60
// Background refresh threshold (seconds)
backgroundRefreshThreshold: 30 // Default: 30
});
`
$3
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| environment | 'development' \| 'staging' \| 'production' | 'production' | API environment target |
| apiProjectKey | string | - | Required. Project key for API authentication |
| apiVersion | 'v2' | 'v2' | API version |
| timeout | number | 30000 | Request timeout (ms) |
| retries | number | 3 | Retry attempts |
| authStorage | TokenStorage | LocalStorage | Token storage implementation |
| dpop.enabled | boolean | true | Enable DPoP security |
| authType | 'user' \| 'admin' | 'user' | Authentication type |
| authProvider | PersAuthProvider | auto-created | Custom auth provider (overrides authType) |
---
Advanced Authentication
$3
For better security and performance, use IndexedDB instead of LocalStorage:
`typescript
import { createPersSDK } from '@explorins/pers-sdk';
import { IndexedDBTokenStorage } from '@explorins/pers-sdk/core';
import { BrowserFetchClientAdapter } from '@explorins/pers-sdk/platform-adapters';
const sdk = createPersSDK(new BrowserFetchClientAdapter(), {
environment: 'production',
apiProjectKey: 'your-key',
authStorage: new IndexedDBTokenStorage() // Secure, async storage
});
`
$3
Implement the TokenStorage interface for custom storage backends:
`typescript
import type { TokenStorage } from '@explorins/pers-sdk/core';
import { AUTH_STORAGE_KEYS, DPOP_STORAGE_KEYS } from '@explorins/pers-sdk/core';
class CustomStorage implements TokenStorage {
// CRITICAL: Set to 'false' for string-only backends (LocalStorage-like)
// Set to 'true' for object-capable backends (IndexedDB-like)
readonly supportsObjects = false;
async get(key: string): Promise {
// Use AUTH_STORAGE_KEYS.ACCESS_TOKEN, DPOP_STORAGE_KEYS.PRIVATE, etc.
return yourBackend.get(key);
}
async set(key: string, value: unknown): Promise {
await yourBackend.set(key, value);
}
async remove(key: string): Promise {
await yourBackend.remove(key);
}
async clear(): Promise {
await yourBackend.clear();
}
}
`
---
Error Handling
The SDK provides structured error types for consistent error handling:
`typescript
import {
PersApiError,
AuthenticationError,
ErrorUtils
} from '@explorins/pers-sdk/core';
try {
const user = await sdk.auth.getCurrentUser();
} catch (error) {
// Check for specific error types
if (error instanceof AuthenticationError) {
// Handle authentication failure (401)
console.error('Auth failed:', error.message);
console.error('Endpoint:', error.endpoint);
console.error('User message:', error.userMessage);
// Redirect to login...
} else if (error instanceof PersApiError) {
// Handle general API errors
console.error('API Error:', error.message);
console.error('Status:', error.status);
console.error('Retryable:', error.retryable);
} else {
// Handle unexpected errors
console.error('Unexpected error:', error);
}
}
// Utility functions for error inspection
const status = ErrorUtils.getStatus(error); // Extract status code
const message = ErrorUtils.getMessage(error); // Extract error message
const retryable = ErrorUtils.isRetryable(error); // Check if retryable
`
$3
| Error Class | Status | Use Case |
|-------------|--------|----------|
| PersApiError | Various | General API request failures |
| AuthenticationError | 401 | Authentication/authorization failures |
| NetworkError | - | Network connectivity issues |
| TokenRefreshNeeded | - | Internal: token refresh required |
| LogoutRequired | - | Internal: session invalidated |
---
Bundle Size
- Installed to node_modules: ~1.9 MB unpacked (includes both ESM + CJS builds, TypeScript definitions, source maps)
- What your bundler actually includes:
- ESM build: ~450 KB (Vite, Rollup, modern Webpack)
- CJS build: ~470 KB (legacy Node.js/Webpack)
- Your bundler only includes ONE of these, not both
- TypeScript definitions: ~510 KB (used by IDE/compiler, not bundled into your app)
- Source maps: ~440 KB (used for debugging, typically excluded from production)
- With Web3 Features: +1.5 MB additional when installing optional @explorins/web3-ts + ethers peer dependencies
- Tree-shaking: The main PersSDK class includes all managers. For smaller bundles, import from domain-specific entry points (e.g., @explorins/pers-sdk/campaign)
- Zero bundled dependencies: All dependencies are peer dependencies
Bottom line: Your production bundle gets ~450 KB from this SDK (ESM), not 1.9 MB. The larger size is what's stored in node_modules, which includes both module formats, type definitions, and development tools.
---
TypeScript Types
All domain interfaces are exported from @explorins/pers-shared. Import types directly for strong typing:
`typescript
import type {
// User & Auth
UserDTO,
SessionAuthContextResponseDTO,
// Business
BusinessDTO,
BusinessTypeDTO,
// Campaigns
CampaignDTO,
CampaignClaimDTO,
CampaignClaimRequestDTO,
// Tokens
TokenDTO,
TokenMetadataDTO,
// Transactions
TransactionDTO,
// Redemptions
RedemptionDTO,
RedemptionRedeemDTO,
// Payments
PaymentIntentDTO,
PurchaseDTO,
PurchaseTokenDTO,
// Tenant
TenantDTO,
TenantClientConfigDTO
} from '@explorins/pers-shared';
`
$3
| Domain | Interface | Description |
|--------|-----------|-------------|
| Auth | UserDTO | Authenticated user profile |
| Auth | SessionAuthContextResponseDTO | Login response with tokens + user/admin |
| Business | BusinessDTO | Business entity with details |
| Business | BusinessTypeDTO | Business category/type definition |
| Campaign | CampaignDTO | Campaign with rewards and rules |
| Campaign | CampaignClaimDTO | User's claim record |
| Campaign | CampaignClaimRequestDTO | Request body for claiming |
| Token | TokenDTO | Token type definition (credit, reward, status) |
| Token | TokenMetadataDTO | On-chain token metadata |
| Transaction | TransactionDTO | Transaction record |
| Redemption | RedemptionDTO | Redemption offer |
| Redemption | RedemptionRedeemDTO | Redemption result |
| Payment | PaymentIntentDTO | Stripe payment intent |
| Payment | PurchaseDTO | Purchase record |
| Tenant | TenantDTO | Tenant configuration |
$3
`typescript
import { createPersSDK } from '@explorins/pers-sdk';
import { BrowserFetchClientAdapter } from '@explorins/pers-sdk/platform-adapters';
import type { CampaignDTO, CampaignClaimRequestDTO } from '@explorins/pers-shared';
const sdk = createPersSDK(new BrowserFetchClientAdapter(), {
apiProjectKey: 'your-key'
});
// Type-safe campaign operations
const response = await sdk.campaigns.getActiveCampaigns();
const campaigns: CampaignDTO[] = response.data;
const claimRequest: CampaignClaimRequestDTO = {
campaignId: campaigns[0].id,
businessId: 'business-123'
};
const claim = await sdk.campaigns.claimCampaign(claimRequest);
`
---
Migration Guide
$3
Version 2.0.0 introduces standardized pagination across all list endpoints. Previously, endpoints returned raw arrays. Now they return PaginatedResponseDTO with pagination metadata.
#### What Changed
All methods returning lists now return paginated responses:
`typescript
// ❌ OLD (v1.x) - Direct array
const businesses: BusinessDTO[] = await sdk.businesses.getActiveBusinesses();
// ✅ NEW (v2.x) - Paginated response
const response: PaginatedResponseDTO = await sdk.businesses.getActiveBusinesses();
const businesses: BusinessDTO[] = response.data;
`
#### Affected Methods
| Manager | Method | Return Type |
|---------|--------|-------------|
| businesses | getActiveBusinesses() | PaginatedResponseDTO |
| businesses | getBusinessTypes() | PaginatedResponseDTO |
| campaigns | getActiveCampaigns() | PaginatedResponseDTO |
| campaigns | getUserClaims() | PaginatedResponseDTO |
| tokens | getTokens() | PaginatedResponseDTO |
| tokens | getRewardTokens() | PaginatedResponseDTO |
| tokens | getStatusTokens() | PaginatedResponseDTO |
| redemptions | getRedemptions() | PaginatedResponseDTO |
| redemptions | getUserRedemptions() | PaginatedResponseDTO |
| transactions | getTransactionHistory() | PaginatedResponseDTO |
| purchases | getAllUserPurchases() | PaginatedResponseDTO |
| purchases | getActivePurchaseTokens() | PaginatedResponseDTO |
| donations | getDonationTypes() | PaginatedResponseDTO |
#### PaginatedResponseDTO Structure
`typescript
import type { PaginatedResponseDTO } from '@explorins/pers-shared';
interface PaginatedResponseDTO {
data: T[]; // Array of results
pagination: {
currentPage: number; // Current page number (1-indexed)
pageSize: number; // Items per page
totalItems: number; // Total number of items across all pages
totalPages: number; // Total number of pages
};
}
`
#### Migration Examples
Before (v1.x):
`typescript
const businesses = await sdk.businesses.getActiveBusinesses();
console.log('Business count:', businesses.length);
businesses.forEach(b => console.log(b.name));
`
After (v2.x):
`typescript
const response = await sdk.businesses.getActiveBusinesses();
console.log('Business count:', response.data.length);
console.log('Total businesses:', response.pagination.totalItems);
response.data.forEach(b => console.log(b.name));
`
With Pagination Parameters:
`typescript
// Fetch page 2 with 20 items per page
const response = await sdk.businesses.getActiveBusinesses({
page: 2,
pageSize: 20
});
console.log(Page ${response.pagination.currentPage} of ${response.pagination.totalPages});
console.log(Showing ${response.data.length} businesses);
`
#### Quick Fix for Existing Code
If you want minimal code changes, extract .data immediately:
`typescript
// Quick adaptation
const businesses = (await sdk.businesses.getActiveBusinesses()).data;
const campaigns = (await sdk.campaigns.getActiveCampaigns()).data;
const tokens = (await sdk.tokens.getTokens()).data;
`
#### Benefits of Pagination
- Performance: Load only what you need, not entire datasets
- Consistency: All list endpoints follow the same pattern
- Metadata: Access total counts without loading all items
- UX: Build proper pagination UI components
---
Documentation
- Getting Started Guide: https://docs.pers.ninja/1.intro
- API Reference: https://docs.pers.ninja/sdk
- TypeDoc API Docs: Generated with npm run docs`