Caso de Uso
Una fintech latinoamericana quiere ofrecer a sus usuarios:- Cuentas con rendimiento en dolares
- Depositos desde cualquier wallet
- Retiros instantaneos
Arquitectura
Copy
┌─────────────────────────────────────────────────────────────────────┐
│ App Movil Fintech │
│ (React Native / Flutter) │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Backend Fintech │
│ (Node.js + Express) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Auth Service │ │ Pan Service │ │ User Service │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ PostgreSQL Database │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Pan API │
│ (Wallets + Intents + Yields) │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Blockchain (DeFi) │
│ Ethereum │ Arbitrum │ Base │ Aave │ Across │
└─────────────────────────────────────────────────────────────────────┘
Implementacion Paso a Paso
1. Configuracion del Proyecto
Copy
# Crear proyecto
mkdir fintech-savings && cd fintech-savings
npm init -y
# Instalar dependencias
npm install express @pan/sdk prisma @prisma/client dotenv zod
npm install -D typescript @types/express @types/node ts-node
# Inicializar TypeScript y Prisma
npx tsc --init
npx prisma init
2. Modelo de Datos
Copy
// prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id String @id @default(cuid())
email String @unique
passwordHash String
// Pan integration
panWalletId String? @unique
walletAddress String?
// Relations
accounts Account[]
transactions Transaction[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Account {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id])
// Tipo de cuenta
type AccountType @default(SAVINGS)
currency String @default("USDC")
// Balance cacheado (se actualiza periodicamente)
balanceCache Float @default(0)
lastSyncAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Transaction {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id])
// Pan tracking
panIntentId String? @unique
// Transaction details
type TransactionType
amount Float
currency String @default("USDC")
status TransactionStatus @default(PENDING)
// Blockchain info
txHashes String[]
gasCostUsd Float?
// Metadata
description String?
errorMessage String?
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
enum AccountType {
SAVINGS
CHECKING
}
enum TransactionType {
DEPOSIT
WITHDRAWAL
INTEREST
}
enum TransactionStatus {
PENDING
PROCESSING
COMPLETED
FAILED
}
3. Servicio de Pan
Copy
// src/services/pan.service.ts
import { Pan, Wallet, Balances, Intent, PanError } from '@pan/sdk';
import { prisma } from '../lib/prisma';
class PanService {
private pan: Pan;
constructor() {
this.pan = new Pan({
apiKey: process.env.PAN_API_KEY!,
environment: process.env.NODE_ENV === 'production' ? 'production' : 'staging'
});
}
// ============================================
// WALLETS
// ============================================
async getOrCreateWallet(userId: string, email?: string): Promise<Wallet> {
// Buscar usuario
const user = await prisma.user.findUnique({
where: { id: userId }
});
if (!user) {
throw new Error('Usuario no encontrado');
}
// Si ya tiene wallet, retornarla
if (user.panWalletId) {
return await this.pan.wallet.get(userId);
}
// Crear wallet nueva
try {
const wallet = await this.pan.wallet.create({
userId,
email: email || user.email,
metadata: {
source: 'fintech-app',
createdAt: new Date().toISOString()
}
});
// Guardar en DB
await prisma.user.update({
where: { id: userId },
data: {
panWalletId: wallet.id,
walletAddress: wallet.address
}
});
// Crear cuenta de ahorros
await prisma.account.create({
data: {
userId,
type: 'SAVINGS',
currency: 'USDC'
}
});
return wallet;
} catch (error) {
if (error instanceof PanError && error.code === 'WALLET_ALREADY_EXISTS') {
// Wallet existe pero no esta en nuestra DB
const wallet = await this.pan.wallet.get(userId);
await prisma.user.update({
where: { id: userId },
data: {
panWalletId: wallet.id,
walletAddress: wallet.address
}
});
return wallet;
}
throw error;
}
}
async getWalletAddress(userId: string): Promise<string> {
const user = await prisma.user.findUnique({
where: { id: userId }
});
if (!user?.walletAddress) {
const wallet = await this.getOrCreateWallet(userId);
return wallet.address;
}
return user.walletAddress;
}
// ============================================
// BALANCES
// ============================================
async getBalances(userId: string): Promise<Balances> {
const user = await prisma.user.findUnique({
where: { id: userId }
});
if (!user?.panWalletId) {
throw new Error('Usuario no tiene wallet');
}
return await this.pan.wallet.getBalances(user.panWalletId);
}
async getTotalUSDC(userId: string): Promise<number> {
const response = await this.getBalances(userId);
let total = 0;
for (const chainData of response.chains) {
const usdc = chainData.tokens.find(t => t.asset === 'USDC');
if (usdc) {
total += parseFloat(usdc.balanceFormatted);
}
}
// Actualizar cache
await prisma.account.updateMany({
where: { userId, currency: 'USDC' },
data: {
balanceCache: total,
lastSyncAt: new Date()
}
});
return total;
}
// ============================================
// DEPOSITOS (LENDING)
// ============================================
async deposit(userId: string, amount: number): Promise<Transaction> {
const user = await prisma.user.findUnique({
where: { id: userId }
});
if (!user?.panWalletId) {
throw new Error('Usuario no tiene wallet');
}
// Verificar fondos
const totalUSDC = await this.getTotalUSDC(userId);
if (totalUSDC < amount) {
throw new Error(`Fondos insuficientes. Tienes ${totalUSDC} USDC, necesitas ${amount} USDC`);
}
// Crear transaccion en DB
const transaction = await prisma.transaction.create({
data: {
userId,
type: 'DEPOSIT',
amount,
currency: 'USDC',
status: 'PENDING',
description: `Deposito de ${amount} USDC en cuenta de ahorros`
}
});
try {
// Crear intent de lending
const intent = await this.pan.lend({
walletId: user.panWalletId,
amount,
asset: 'USDC'
});
// Actualizar transaccion con intent
await prisma.transaction.update({
where: { id: transaction.id },
data: {
panIntentId: intent.id,
status: 'PROCESSING'
}
});
// Iniciar monitoreo en background
this.monitorIntent(intent.id, transaction.id);
return transaction;
} catch (error) {
await prisma.transaction.update({
where: { id: transaction.id },
data: {
status: 'FAILED',
errorMessage: error instanceof Error ? error.message : 'Error desconocido'
}
});
throw error;
}
}
// ============================================
// RETIROS (WITHDRAW)
// ============================================
async withdraw(userId: string, amount: number, destinationAddress?: string): Promise<Transaction> {
const user = await prisma.user.findUnique({
where: { id: userId }
});
if (!user?.panWalletId) {
throw new Error('Usuario no tiene wallet');
}
// Crear transaccion
const transaction = await prisma.transaction.create({
data: {
userId,
type: 'WITHDRAWAL',
amount,
currency: 'USDC',
status: 'PENDING',
description: `Retiro de ${amount} USDC`
}
});
try {
// Crear intent de withdraw
const intent = await this.pan.withdraw({
walletId: user.panWalletId,
amount,
asset: 'USDC',
destination: destinationAddress || user.walletAddress!
});
await prisma.transaction.update({
where: { id: transaction.id },
data: {
panIntentId: intent.id,
status: 'PROCESSING'
}
});
this.monitorIntent(intent.id, transaction.id);
return transaction;
} catch (error) {
await prisma.transaction.update({
where: { id: transaction.id },
data: {
status: 'FAILED',
errorMessage: error instanceof Error ? error.message : 'Error desconocido'
}
});
throw error;
}
}
// ============================================
// YIELDS
// ============================================
async getCurrentAPY(): Promise<number> {
const yields = await this.pan.yields.getAll();
// Obtener mejor APY de USDC
const usdcYields = yields.rates.filter(y => y.asset === 'USDC');
if (usdcYields.length === 0) {
return 0;
}
return Math.max(...usdcYields.map(y => y.apy));
}
// ============================================
// MONITOREO
// ============================================
private async monitorIntent(intentId: string, transactionId: string): Promise<void> {
const maxAttempts = 60;
const interval = 5000;
for (let i = 0; i < maxAttempts; i++) {
try {
const intent = await this.pan.getIntent(intentId);
if (intent.status === 'completed') {
await prisma.transaction.update({
where: { id: transactionId },
data: {
status: 'COMPLETED',
txHashes: intent.txHashes || [],
gasCostUsd: intent.gasCostUsd,
completedAt: new Date()
}
});
return;
}
if (intent.status === 'failed') {
await prisma.transaction.update({
where: { id: transactionId },
data: {
status: 'FAILED',
errorMessage: intent.error?.message || 'Intent fallido'
}
});
return;
}
await new Promise(r => setTimeout(r, interval));
} catch (error) {
console.error(`Error monitoreando intent ${intentId}:`, error);
}
}
// Timeout
await prisma.transaction.update({
where: { id: transactionId },
data: {
status: 'FAILED',
errorMessage: 'Timeout esperando confirmacion'
}
});
}
}
export const panService = new PanService();
4. API Routes
Copy
// src/routes/accounts.ts
import { Router } from 'express';
import { z } from 'zod';
import { panService } from '../services/pan.service';
import { authMiddleware } from '../middleware/auth';
const router = Router();
// Obtener cuenta y balances
router.get('/', authMiddleware, async (req, res) => {
try {
const userId = req.user!.id;
const [wallet, totalUSDC, apy] = await Promise.all([
panService.getOrCreateWallet(userId),
panService.getTotalUSDC(userId),
panService.getCurrentAPY()
]);
res.json({
address: wallet.address,
balance: {
total: totalUSDC,
currency: 'USDC',
formatted: `$${totalUSDC.toFixed(2)}`
},
apy: {
current: apy,
formatted: `${(apy * 100).toFixed(2)}%`
},
estimatedMonthlyEarnings: totalUSDC * apy / 12
});
} catch (error) {
console.error('Error obteniendo cuenta:', error);
res.status(500).json({ error: 'Error obteniendo cuenta' });
}
});
// Depositar en cuenta de ahorros
const depositSchema = z.object({
amount: z.number().positive().max(1000000)
});
router.post('/deposit', authMiddleware, async (req, res) => {
try {
const { amount } = depositSchema.parse(req.body);
const userId = req.user!.id;
const transaction = await panService.deposit(userId, amount);
res.json({
transactionId: transaction.id,
status: 'processing',
message: `Depositando ${amount} USDC en tu cuenta de ahorros`
});
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ error: 'Monto invalido' });
}
console.error('Error en deposito:', error);
res.status(500).json({
error: error instanceof Error ? error.message : 'Error en deposito'
});
}
});
// Retirar de cuenta de ahorros
const withdrawSchema = z.object({
amount: z.number().positive().max(1000000),
destinationAddress: z.string().optional()
});
router.post('/withdraw', authMiddleware, async (req, res) => {
try {
const { amount, destinationAddress } = withdrawSchema.parse(req.body);
const userId = req.user!.id;
const transaction = await panService.withdraw(userId, amount, destinationAddress);
res.json({
transactionId: transaction.id,
status: 'processing',
message: `Retirando ${amount} USDC`
});
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ error: 'Datos invalidos' });
}
console.error('Error en retiro:', error);
res.status(500).json({
error: error instanceof Error ? error.message : 'Error en retiro'
});
}
});
// Historial de transacciones
router.get('/transactions', authMiddleware, async (req, res) => {
try {
const userId = req.user!.id;
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
const offset = parseInt(req.query.offset as string) || 0;
const transactions = await prisma.transaction.findMany({
where: { userId },
orderBy: { createdAt: 'desc' },
take: limit,
skip: offset
});
res.json({
transactions: transactions.map(tx => ({
id: tx.id,
type: tx.type,
amount: tx.amount,
currency: tx.currency,
status: tx.status,
description: tx.description,
createdAt: tx.createdAt,
completedAt: tx.completedAt
})),
pagination: {
limit,
offset,
hasMore: transactions.length === limit
}
});
} catch (error) {
console.error('Error obteniendo transacciones:', error);
res.status(500).json({ error: 'Error obteniendo transacciones' });
}
});
// Estado de transaccion especifica
router.get('/transactions/:id', authMiddleware, async (req, res) => {
try {
const { id } = req.params;
const userId = req.user!.id;
const transaction = await prisma.transaction.findFirst({
where: { id, userId }
});
if (!transaction) {
return res.status(404).json({ error: 'Transaccion no encontrada' });
}
res.json(transaction);
} catch (error) {
console.error('Error obteniendo transaccion:', error);
res.status(500).json({ error: 'Error obteniendo transaccion' });
}
});
export default router;
5. App Principal
Copy
// src/app.ts
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import accountsRouter from './routes/accounts';
const app = express();
// Middleware
app.use(helmet());
app.use(cors());
app.use(express.json());
// Routes
app.use('/api/accounts', accountsRouter);
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Error handler
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
console.error('Error:', err);
res.status(500).json({ error: 'Error interno del servidor' });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Servidor corriendo en puerto ${PORT}`);
});
6. Variables de Entorno
Copy
# .env
NODE_ENV=development
PORT=3000
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/fintech
# Pan API
PAN_API_KEY=pan_sk_test_xxxxx
# JWT (para autenticacion de usuarios)
JWT_SECRET=tu-secreto-muy-seguro
Flujo de Usuario
Registro y Creacion de Wallet
Copy
// El usuario se registra en la app
const user = await registerUser(email, password);
// Automaticamente creamos su wallet Pan
const wallet = await panService.getOrCreateWallet(user.id, email);
// Usuario puede ver su direccion para depositos
console.log(`Direccion para depositos: ${wallet.address}`);
Deposito en Cuenta de Ahorros
Copy
// Usuario tiene USDC en su wallet (envio externo o compra)
// Quiere depositarlo en la cuenta de ahorros
// 1. Verificar balance
const balance = await panService.getTotalUSDC(userId);
console.log(`Balance disponible: ${balance} USDC`);
// 2. Depositar
const tx = await panService.deposit(userId, 100);
console.log(`Deposito iniciado: ${tx.id}`);
// 3. El intent se ejecuta automaticamente
// - Pan consolida USDC de todas las chains
// - Deposita en Aave para generar rendimiento
// - La transaccion se marca como completada
Consulta de Rendimientos
Copy
// Obtener APY actual
const apy = await panService.getCurrentAPY();
console.log(`APY actual: ${(apy * 100).toFixed(2)}%`);
// Calcular ganancias estimadas
const balance = 1000; // USDC
const monthlyEarnings = balance * apy / 12;
const yearlyEarnings = balance * apy;
console.log(`Ganancias mensuales estimadas: $${monthlyEarnings.toFixed(2)}`);
console.log(`Ganancias anuales estimadas: $${yearlyEarnings.toFixed(2)}`);
Retiro
Copy
// Usuario quiere retirar a su wallet
const tx = await panService.withdraw(userId, 50);
// O a una direccion externa
const tx = await panService.withdraw(userId, 50, '0xExternalAddress...');
Consideraciones de Produccion
Seguridad
Copy
// Rate limiting
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutos
max: 100, // max requests por ventana
message: 'Demasiadas solicitudes, intenta mas tarde'
});
app.use('/api/', limiter);
Monitoreo
Copy
// Logging estructurado
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL || 'info'
});
// Metricas
import { Counter, Histogram } from 'prom-client';
const depositCounter = new Counter({
name: 'deposits_total',
help: 'Total de depositos'
});
const depositDuration = new Histogram({
name: 'deposit_duration_seconds',
help: 'Duracion de depositos'
});
Escalabilidad
Copy
// Cola de trabajos para monitoreo de intents
import Bull from 'bull';
const intentQueue = new Bull('intent-monitor', {
redis: process.env.REDIS_URL
});
intentQueue.process(async (job) => {
const { intentId, transactionId } = job.data;
await panService.monitorIntent(intentId, transactionId);
});
// En lugar de monitorear en el mismo proceso
intentQueue.add({ intentId, transactionId });
Resultado Final
Con esta implementacion, tu fintech puede:- Crear cuentas automaticamente para cada usuario
- Aceptar depositos en cualquier chain soportada
- Generar rendimiento automatico con los mejores APYs
- Procesar retiros de forma instantanea
- Mostrar historial completo de transacciones
- Gestionar wallets manualmente
- Interactuar directamente con blockchains
- Implementar bridges o swaps
- Integrar protocolos DeFi individualmente
