API Layer & Middleware
The API layer handles HTTP requests through Express routes and middleware. All endpoints are versioned under /api/v1/.
API Structure
src/api/
├── index.ts # Route aggregator
└── v1/
├── agents/ # Agent management
├── agent-balances/ # Balance queries
├── agent-positions/ # Position queries
├── agent-transactions/ # Transaction history
├── agent-historical-swaps/ # Completed trades
├── api-keys/ # API key management
├── auth/ # Authentication
├── data-sources/ # Price feed sources
├── health/ # Health checks
├── metrics/ # Prometheus metrics
├── price-feeds/ # Current prices
├── trading-signals/ # Signal management
├── wallets/ # Wallet operations
└── webhooks/ # Webhook testingEndpoint Reference
Authentication (/api/v1/auth)
| Method | Endpoint | Description | Auth |
|---|---|---|---|
POST | /register | Create new user | None |
POST | /login | Authenticate user | None |
POST | /tokens/refresh | Refresh access token | Refresh token |
GET | /me | Get current user | JWT |
Agents (/api/v1/agents)
| Method | Endpoint | Description | Auth |
|---|---|---|---|
POST | / | Create agent | JWT |
GET | / | List user's agents | JWT |
GET | /:id | Get agent details | JWT |
PUT | /:id | Update agent | JWT |
DELETE | /:id | Delete agent | JWT |
GET | /:id/config | Get trading config | JWT |
PUT | /:id/config | Update trading config | JWT |
GET | /:id/positions | Get open positions | JWT |
GET | /:id/performance | Get performance metrics | JWT |
GET | /:id/balance-history | Get balance history | JWT |
Trading Signals (/api/v1/trading-signals)
| Method | Endpoint | Description | Auth |
|---|---|---|---|
POST | / | Create signal | API Key |
GET | / | List signals | JWT |
GET | /:id | Get signal details | JWT |
DELETE | /:id | Delete signal | JWT |
GET | /export | Export signals CSV | JWT |
Wallets (/api/v1/wallets)
| Method | Endpoint | Description | Auth |
|---|---|---|---|
GET | / | List available wallets | JWT |
POST | /assign | Assign wallet to agent | JWT |
POST | /reset | Reset simulation wallet | JWT |
POST | /check-deposits | Check for SOL deposits | JWT |
Health (/api/v1/health)
| Method | Endpoint | Description | Auth |
|---|---|---|---|
GET | / | Full health check | None |
GET | /live | Liveness probe | None |
GET | /ready | Readiness probe | None |
Route Definition Pattern
Routes follow a consistent pattern with middleware chaining:
// src/api/v1/agents/routes.ts
import { Router } from 'express';
import { authenticate } from '@/middleware/auth.js';
import { validate } from '@/middleware/validation.js';
import { CreateAgentSchema, UpdateAgentSchema } from 'nexgent-open-source-trading-engine/shared';
const router = Router();
// Create agent: Auth → Validate → Handler
router.post('/', authenticate, validate(CreateAgentSchema), createAgent);
// List agents: Auth → Handler
router.get('/', authenticate, listAgents);
// Get agent: Auth → Handler
router.get('/:id', authenticate, getAgent);
// Update agent: Auth → Validate → Handler
router.put('/:id', authenticate, validate(UpdateAgentSchema), updateAgent);
// Delete agent: Auth → Handler
router.delete('/:id', authenticate, deleteAgent);
export default router;Middleware
Authentication (auth.ts)
Verifies JWT tokens from Authorization header:
export function authenticate(
req: AuthenticatedRequest,
res: Response,
next: NextFunction
): void {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Authorization required' });
}
const token = authHeader.slice(7);
try {
const payload = verifyToken(token);
// Check token type
if (payload.type !== 'access') {
return res.status(401).json({ error: 'Invalid token type' });
}
// Check blacklist
if (payload.jti) {
const isBlacklisted = await redisTokenService.isAccessTokenBlacklisted(payload.jti);
if (isBlacklisted) {
return res.status(401).json({ error: 'Token revoked' });
}
}
req.user = { id: payload.userId, email: payload.email };
next();
} catch (error) {
return res.status(401).json({ error: 'Invalid token' });
}
}API Key Authentication (api-key-auth.ts)
Authenticates using API keys (for programmatic access):
export function authenticateApiKey(
req: AuthenticatedRequest,
res: Response,
next: NextFunction
): void {
const apiKey = extractApiKey(req); // X-API-Key header or Bearer token
if (!apiKey) {
return res.status(401).json({ error: 'API key required' });
}
const keyData = await verifyApiKey(apiKey);
if (!keyData) {
return res.status(401).json({ error: 'Invalid API key' });
}
req.user = { id: keyData.userId, email: keyData.email };
req.apiKeyId = keyData.id;
next();
}
// With scope requirement
export function authenticateApiKeyWithScope(requiredScope: string) {
return async (req, res, next) => {
// ... same as above, plus:
if (!hasScope(keyData.scopes, requiredScope)) {
return res.status(403).json({ error: `Missing scope: ${requiredScope}` });
}
next();
};
}Validation (validation.ts)
Validates request bodies using Zod schemas:
export function validate(schema: ZodSchema) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: 'Validation failed',
details: result.error.flatten(),
});
}
req.body = result.data; // Replace with validated data
next();
};
}Zod schemas are defined in nexgent-open-source-trading-engine/shared and shared between frontend and backend for consistent validation.
Rate Limiting (rate-limiter.ts)
Prevents abuse with per-IP rate limits:
export function rateLimiter(
maxRequests: number = 10,
windowMs: number = 60000 // 1 minute
) {
return (req: Request, res: Response, next: NextFunction) => {
const ip = req.ip || 'unknown';
const key = `${ip}:${req.path}`;
let entry = rateLimitStore.get(key);
if (!entry || entry.resetTime < Date.now()) {
entry = { count: 1, resetTime: Date.now() + windowMs };
rateLimitStore.set(key, entry);
return next();
}
entry.count++;
if (entry.count > maxRequests) {
const retryAfter = Math.ceil((entry.resetTime - Date.now()) / 1000);
return res.status(429).json({
error: 'Too many requests',
retryAfter,
});
}
next();
};
}
// Pre-configured limiters
export const walletRateLimiter = rateLimiter(5, 60000);
export const signalsApiKeyRateLimiter = apiKeyRateLimiter(120, 60000);Request Logger (request-logger.ts)
Structured logging for all requests:
export function requestLogger(req: Request, res: Response, next: NextFunction) {
const requestId = req.headers['x-request-id'] || randomUUID();
req.headers['x-request-id'] = requestId;
const start = Date.now();
res.on('finish', () => {
logger.info({
requestId,
method: req.method,
path: req.path,
statusCode: res.statusCode,
duration: Date.now() - start,
userAgent: req.headers['user-agent'],
}, 'Request completed');
});
next();
}Error Handler (error-handler.ts)
Global error handling:
export function errorHandler(
err: Error,
req: Request,
res: Response,
next: NextFunction
) {
logger.error({
error: err.message,
stack: err.stack,
path: req.path,
method: req.method,
}, 'Unhandled error');
// Don't leak internal errors in production
const message = process.env.NODE_ENV === 'production'
? 'Internal server error'
: err.message;
res.status(500).json({ error: message });
}
export function notFoundHandler(req: Request, res: Response) {
res.status(404).json({ error: 'Not found' });
}Handler Pattern
Handlers extract validated data and delegate to domain services:
// src/api/v1/agents/handlers/create.ts
export async function createAgent(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const userId = req.user!.id;
const { name, tradingMode } = req.body; // Validated by middleware
// Delegate to domain service
const agent = await agentService.createAgent({
userId,
name,
tradingMode,
});
res.status(201).json(agent);
} catch (error) {
if (error instanceof AgentError) {
res.status(400).json({ error: error.message });
return;
}
throw error; // Let error handler catch it
}
}Response Format
All endpoints return consistent JSON responses:
Success
// Single resource
{ "id": "uuid", "name": "Agent 1", ... }
// List
{ "items": [...], "total": 10 }
// Action result
{ "success": true, "message": "..." }Error
{
"error": "Error message",
"details": { ... } // Optional validation details
}CORS Configuration
const corsOptions = {
origin: process.env.CORS_ORIGIN
? process.env.CORS_ORIGIN.split(',')
: ['http://localhost:3000'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
};
app.use(cors(corsOptions));