npm package: @pocketsignals/broker-client
Description: Webhook server for receiving PocketSignals trading signals and dispatch client
Author: PocketSignals
Latest version: 2.0.0

# @pocketsignals/broker-client

Webhook server and dispatch client for receiving PocketSignals trading signals.

## Contract Version

**v2.0.0** - Push/Webhook Architecture

## v2.0.0 Breaking Changes

- **Architecture Change**: PocketSignals now PUSHES signals to your webhook server
- `BrokerPollingClient` is **deprecated** - use `WebhookServer` instead
- New push model is more efficient and lower latency

## Installation

```bash
npm install @pocketsignals/broker-client
```

## Quick Start - Webhook Server (Recommended)

The webhook server receives push signals from PocketSignals:

```typescript
import { WebhookServer } from '@pocketsignals/broker-client';

const server = new WebhookServer({
  config: {
    port: 8080,
    serviceKey: process.env.POCKETSIGNALS_SERVICE_KEY!,
    basePath: '/broker/signals', // Optional, this is the default
  },
  handlers: {
    onEntry: async (signal) => {
      console.log('Entry signal received:', {
        symbol: signal.symbol,
        direction: signal.direction,
        price: signal.price,
        tsid: signal.tsid,
      });
      
      // Execute your trade logic here...
      const result = await executeAlpacaOrder(signal);
      
      return {
        success: true,
        signalId: signal.signalId,
        accepted: true,
      };
    },
    
    onExit: async (signal) => {
      console.log('Exit signal received:', {
        symbol: signal.symbol,
        exitReason: signal.exitReason,
        tsid: signal.tsid,
      });
      
      // Close your position here...
      const result = await closeAlpacaPosition(signal);
      
      return {
        success: true,
        signalId: signal.signalId,
        accepted: true,
      };
    },
    
    onStatus: async (signal) => {
      console.log('Status update:', signal.status);
      return { success: true };
    },
    
    onError: (error, payload) => {
      console.error('Handler error:', error, payload);
    },
  },
});

// Start the server
await server.start();
console.log(`Webhook server running on port ${server.getPort()}`);

// Check metrics
const metrics = server.getMetrics();
console.log('Signals received:', metrics.signalsReceived);

// Graceful shutdown
process.on('SIGTERM', async () => {
  await server.stop();
});
```

## Webhook Endpoints

PocketSignals will push signals to:

| Endpoint | Description |
|----------|-------------|
| `POST /broker/signals/entry` | Entry signals (buy/call) |
| `POST /broker/signals/exit` | Exit signals (sell/close) |
| `POST /broker/signals/status` | Status updates |

Health endpoints:

| Endpoint | Description |
|----------|-------------|
| `GET /health` | Health check |
| `GET /ready` | Readiness check |
| `GET /metrics` | Server metrics |

## Entry Signal Payload

```typescript
interface EntrySignalWebhook {
  signalType: 'ENTRY';
  signalId: string;
  idempotencyKey: string;
  tsid: string;
  userId: string;
  symbol: string;
  assetType: 'options' | 'crypto';
  direction: 'BUY' | 'SELL' | 'CALL' | 'PUT';
  quantity: number;
  price: number;
  confidence: number;
  rationale: string;
  contractParams?: {
    strike: number;
    expiry: string;
    type: 'CALL' | 'PUT';
    contractSymbol?: string;
  };
  riskManagement?: RiskManagement;
  contractVersion: string;
  timestamp: number;
}
```

## Exit Signal Payload

```typescript
interface ExitSignalWebhook {
  signalType: 'EXIT';
  signalId: string;
  idempotencyKey: string;
  tsid: string;
  userId: string;
  symbol: string;
  assetType: 'options' | 'crypto';
  quantity: number;
  exitReason: 'AI_COACH' | 'STOP_LOSS' | 'TAKE_PROFIT' | 'CIRCUIT_BREAKER' | 'MANUAL' | 'EXPIRY';
  riskManagement?: RiskManagement;
  contractVersion: string;
  timestamp: number;
}
```

## HMAC Authentication

All incoming requests include HMAC signatures for security:

| Header | Description |
|--------|-------------|
| `X-Broker-Signature` | HMAC-SHA256 signature of `<timestamp>.<body>` |
| `X-Broker-Timestamp` | Unix timestamp in milliseconds |
| `Content-Type` | Must be `application/json` |

### Security Guarantees

- **Timestamp validation**: ±30 second drift tolerance (configurable)
- **Replay protection**: Duplicate signature+timestamp+body combinations are blocked
- **Timing-safe comparison**: Constant-time signature verification to prevent timing attacks

### Error Codes

| HTTP Code | Description |
|-----------|-------------|
| `401` | Missing or invalid signature |
| `408` | Timestamp expired (outside ±30s window) |
| `409` | Duplicate request (replay attack blocked) |

The WebhookServer automatically verifies signatures. For custom verification:

```typescript
import { WebhookHmacVerifier } from '@pocketsignals/broker-client';

const verifier = new WebhookHmacVerifier({
  serviceKey: process.env.POCKETSIGNALS_SERVICE_KEY!,
  maxTimestampDriftMs: 30000, // 30 seconds (default, contract-compliant)
});

const result = verifier.verify(
  req.headers['x-broker-signature'],
  req.headers['x-broker-timestamp'],
  requestBody
);

if (!result.valid) {
  console.error('Auth failed:', result.error);
}
```

## Idempotency / Duplicate Suppression

The WebhookServer includes built-in duplicate detection using idempotency keys.

For single-instance deployments (default):

```typescript
import { InMemoryIdempotencyStorage } from '@pocketsignals/broker-client';

const storage = new InMemoryIdempotencyStorage({
  defaultTtlMs: 3600000, // 1 hour
});
```

For multi-instance deployments with Redis:

```typescript
import Redis from 'ioredis';
import { RedisIdempotencyStorage } from '@pocketsignals/broker-client';

const redis = new Redis(process.env.REDIS_URL);
const storage = new RedisIdempotencyStorage(redis, {
  prefix: 'ps:idempotency:',
  defaultTtlMs: 3600000,
});

const server = new WebhookServer({
  config: { port: 8080, serviceKey: '...' },
  handlers: { ... },
  idempotencyStorage: storage,
});
```

## Environment Variables

| Variable | Description | Required |
|----------|-------------|----------|
| `POCKETSIGNALS_SERVICE_KEY` | HMAC secret for signature verification | Yes |

## Response Format

Handlers should return a WebhookResponse:

```typescript
interface WebhookResponse {
  success: boolean;
  signalId?: string;
  accepted?: boolean;     // True if signal was acted upon
  duplicate?: boolean;    // True if duplicate was detected
  error?: string;         // Error message if failed
  retryAfterMs?: number;  // Request retry after this delay
}
```

Return codes:
- `200` - Signal processed successfully
- `400` - Bad request / handler rejected
- `401` - Authentication failed
- `408` - Timestamp too old
- `409` - Duplicate request
- `503` - Temporary failure (retry)

## Retry Semantics

PocketSignals will retry failed webhook deliveries with exponential backoff:

| Attempt | Delay |
|---------|-------|
| 1 | Immediate |
| 2 | 5 seconds |
| 3 | 15 seconds |
| 4 | 60 seconds |
| 5 | 300 seconds |

Return `503 Service Unavailable` with optional `retryAfterMs` in response body to request custom retry timing.

## Production Requirements

- **TLS**: Webhook endpoints MUST be served over HTTPS in production
- **Timeouts**: Handler execution should complete within 10 seconds
- **Idempotency**: Use Redis storage for multi-instance deployments
- **Monitoring**: Use `/metrics` endpoint for observability

## Migration from v1.x (Polling Client)

```typescript
// OLD (deprecated) - Polling for commands
import { BrokerPollingClient } from '@pocketsignals/broker-client';

const client = new BrokerPollingClient({ ... });
while (true) {
  const result = await client.pollCommands();
  // Process commands...
  await sleep(5000);
}

// NEW (v2.0.0) - Receive pushed signals
import { WebhookServer } from '@pocketsignals/broker-client';

const server = new WebhookServer({
  config: { port: 8080, serviceKey: '...' },
  handlers: {
    onEntry: async (signal) => { /* handle entry */ },
    onExit: async (signal) => { /* handle exit */ },
  },
});
await server.start();
```

## Dispatch Client (PocketSignals Internal Use)

For PocketSignals to push signals to Trade Automator:

```typescript
import axios from 'axios';
import { BrokerDispatchClient } from '@pocketsignals/broker-client';

const client = new BrokerDispatchClient({
  config: {
    enabled: true,
    brokerBaseUrl: process.env.BROKER_BASE_URL!,
    serviceKey: process.env.BROKER_SERVICE_KEY!,
    callbackUrl: process.env.BROKER_CALLBACK_URL!,
    environment: 'production',
  },
  axios: axios.create(),
  logger: console,
});

const result = await client.dispatchSignal({ ... });
```

## API Reference

### WebhookServer

```typescript
class WebhookServer {
  constructor(options: WebhookServerOptions);
  start(): Promise<void>;
  stop(): Promise<void>;
  isRunning(): boolean;
  getPort(): number;
  getMetrics(): WebhookMetrics;
}
```

### WebhookHmacVerifier

```typescript
class WebhookHmacVerifier {
  constructor(config: { serviceKey: string; maxTimestampDriftMs?: number });
  verify(signature: string, timestamp: string, body: string): HmacVerifyResult;
  sign(timestamp: number, body: string): string;
  stop(): void;
}
```

### InMemoryIdempotencyStorage

```typescript
class InMemoryIdempotencyStorage implements IdempotencyStorage {
  constructor(options?: { defaultTtlMs?: number; cleanupIntervalMs?: number });
  has(key: string): Promise<boolean>;
  set(key: string, ttlMs?: number): Promise<void>;
  delete(key: string): Promise<void>;
  size(): number;
  stop(): void;
}
```

## Version Compatibility

| Package Version | Contract Version | Architecture |
|-----------------|------------------|--------------|
| 2.0.x | 2.0.0 | Push/Webhook (recommended) |
| 1.4.x | 1.4.0 | Polling (deprecated) |
| 1.3.x | 1.3.0 | Polling |

## License

MIT