# 55 — Pepperstone Broker Integration (cTrader Open API)

> **Status (2026-05-07): SHIPPED.** OAuth 2.0 flow complete, token refresh scheduler wired, UI integrated.

This document covers the Pepperstone broker integration using the cTrader Open API. Unlike Alpaca/Binance/IBKR which use API key/secret pairs, Pepperstone uses **OAuth 2.0 with PKCE** for secure authorization.

---

## Overview

Pepperstone is a forex and CFD broker that uses the cTrader trading platform. Integration is via the [cTrader Open API](https://help.ctrader.com/open-api/), which provides:

- OAuth 2.0 authentication (no API keys to manage)
- REST API for account data and order management
- Support for demo and live accounts
- 70+ forex pairs, indices, commodities, crypto CFDs

### Key Differences from Other Brokers

| Aspect | Alpaca/Binance/IBKR | Pepperstone (cTrader) |
|--------|---------------------|----------------------|
| Auth method | API Key + Secret | OAuth 2.0 (access + refresh tokens) |
| Token storage | Single encrypted pair | Encrypted tokens with expiry tracking |
| User flow | Enter keys manually | "Connect with Pepperstone" → OAuth redirect |
| Token refresh | N/A | Auto-refresh before 30-day expiry |
| Credential management | User copies from broker | One-click authorization |

---

## Architecture

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                           USER FLOW                                          │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  1. User clicks "Connect with Pepperstone" on /settings/brokers              │
│                              ↓                                               │
│  2. Frontend calls POST /api/predict/v1/user/brokers/pepperstone/oauth/initiate │
│     - Generates secure state + PKCE code verifier                            │
│     - Stores in broker_oauth_states table (10 min expiry)                    │
│     - Returns cTrader authorization URL                                      │
│                              ↓                                               │
│  3. Frontend redirects user to cTrader authorization page                    │
│     - User logs in with Pepperstone credentials                              │
│     - User authorizes trading access                                         │
│                              ↓                                               │
│  4. cTrader redirects back to callback URL with auth code                    │
│     GET /api/predict/v1/user/brokers/pepperstone/oauth/callback?code=...&state=... │
│                              ↓                                               │
│  5. Callback handler:                                                        │
│     - Validates state (CSRF protection, single-use)                          │
│     - Exchanges auth code for tokens via cTrader token endpoint              │
│     - Encrypts tokens (AES-256-GCM) and stores in user_broker_credentials    │
│     - Redirects to /settings/brokers?oauth=success                           │
│                              ↓                                               │
│  6. User is now connected - can test connection, trade via algorithms        │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘
```

### Token Refresh Flow

cTrader access tokens expire after 30 days. A scheduled job refreshes tokens proactively:

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                      SCHEDULED TOKEN REFRESH (every 6 hours)                 │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  1. Job queries user_broker_credentials for tokens expiring in < 48 hours    │
│  2. For each expiring token:                                                 │
│     - Decrypts refresh_token                                                 │
│     - Calls cTrader token endpoint with grant_type=refresh_token             │
│     - Encrypts new tokens and updates database                               │
│  3. Logs results to broker_audit_log                                         │
│  4. Also cleans up expired/used OAuth states                                 │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘
```

---

## Database Schema

### Migration 189: `db/migrations/189_pepperstone_broker.sql`

#### New columns on `user_broker_credentials`:

| Column | Type | Purpose |
|--------|------|---------|
| `auth_type` | VARCHAR(20) | `'api_key'` or `'oauth'` - discriminator |
| `encrypted_access_token` | TEXT | AES-256-GCM encrypted OAuth access token |
| `encrypted_refresh_token` | TEXT | AES-256-GCM encrypted OAuth refresh token |
| `token_expires_at` | TIMESTAMP | When access token expires (for refresh scheduling) |

#### New table `broker_oauth_states`:

| Column | Type | Purpose |
|--------|------|---------|
| `id` | UUID | Primary key |
| `user_id` | UUID | FK to auth.users |
| `broker_id` | VARCHAR(40) | FK to broker_configs |
| `state` | VARCHAR(100) | Random state parameter (CSRF protection) |
| `redirect_uri` | TEXT | Where to redirect after OAuth (optional) |
| `code_verifier` | VARCHAR(200) | PKCE code verifier (base64url) |
| `expires_at` | TIMESTAMP | State expires after 10 minutes |
| `used_at` | TIMESTAMP | Set when consumed (single-use enforcement) |

#### Seed data in `broker_configs`:

```sql
INSERT INTO predict.broker_configs (broker_id, display_name, ...) VALUES (
  'pepperstone',
  'Pepperstone (cTrader)',
  FALSE,  -- Admin must enable after registering OAuth app
  FALSE,  -- live_trading_enabled
  '{"forex_bps": 0, "cfd_bps": 0}'::jsonb,  -- Spread-based pricing
  50,     -- rate_limit_rps
  '["AU", "UK", "EU", "GLOBAL"]'::jsonb,
  'https://demo.ctraderapi.com',
  'https://live.ctraderapi.com',
  100000, -- max_order_notional_usd
  '["forex", "cfd", "index", "commodity", "crypto"]'::jsonb,
  'Pepperstone via cTrader Open API...'
);
```

---

## API Endpoints

### OAuth Flow Endpoints

| Method | Path | Auth | Purpose |
|--------|------|------|---------|
| POST | `/api/predict/v1/user/brokers/pepperstone/oauth/initiate` | bearer+active | Start OAuth flow, returns authorization URL |
| GET | `/api/predict/v1/user/brokers/pepperstone/oauth/callback` | public | Handle cTrader redirect, exchange code for tokens |

### Initiate Request/Response

**Request:**
```json
POST /api/predict/v1/user/brokers/pepperstone/oauth/initiate
{
  "redirectAfterAuth": "/settings/brokers"  // optional
}
```

**Response:**
```json
{
  "authorizationUrl": "https://openapi.ctrader.com/apps/auth?client_id=...&state=...&code_challenge=...",
  "state": "oauthstate_abc123...",
  "expiresInSeconds": 600
}
```

### Callback Flow

The callback endpoint handles the redirect from cTrader:

1. **Success case:** `?code=AUTH_CODE&state=STATE`
   - Validates state, exchanges code for tokens, stores encrypted
   - Redirects to `/settings/brokers?oauth=success&broker=pepperstone`

2. **Error case:** `?error=access_denied&error_description=User%20denied`
   - Redirects to `/settings/brokers?oauth=error&error=User%20denied`

### Existing Broker Endpoints (work with Pepperstone)

| Method | Path | Purpose |
|--------|------|---------|
| GET | `/api/predict/v1/user/brokers` | List connected brokers (includes Pepperstone) |
| PUT | `/api/predict/v1/user/brokers/pepperstone` | Test saved OAuth connection |
| DELETE | `/api/predict/v1/user/brokers/pepperstone` | Disconnect (deletes tokens) |
| PUT | `/api/predict/v1/user/brokers/pepperstone/mode` | Switch paper/live mode |
| PUT | `/api/predict/v1/user/brokers/pepperstone/live-ack` | Acknowledge live trading risk |
| POST | `/api/predict/v1/user/brokers/pepperstone/go-live` | MFA-verified live mode switch |

---

## File Structure

### Backend (`packages/be/src/brokers/`)

| File | Purpose |
|------|---------|
| `pepperstone.ts` | `PepperstoneAdapter` implementing `BrokerAdapter` interface |
| `pepperstone-oauth.ts` | OAuth flow: state management, token exchange, refresh |
| `pepperstone-types.ts` | cTrader API TypeScript types |
| `service.ts` | Updated to handle OAuth credentials and instantiate adapter |
| `types.ts` | Extended `BrokerCredentials` with OAuth fields |
| `api/handlers.ts` | OAuth initiate/callback handlers |

### Scheduler (`packages/be/src/scheduler/`)

| File | Purpose |
|------|---------|
| `jobs/pepperstone-token-refresh.ts` | Token refresh job implementation |
| `index.ts` | Job registration (every 6 hours) |

### API Routes (`apps/web/src/app/api/predict/v1/user/brokers/pepperstone/oauth/`)

| File | Purpose |
|------|---------|
| `initiate/route.ts` | POST handler to start OAuth |
| `callback/route.ts` | GET handler for cTrader redirect |

### Frontend (`apps/web/src/app/settings/brokers/`)

| File | Changes |
|------|---------|
| `page.tsx` | Added Pepperstone guide, OAuth connect button, callback handling |

---

## BrokerAdapter Implementation

The `PepperstoneAdapter` implements the standard `BrokerAdapter` interface:

```typescript
class PepperstoneAdapter implements BrokerAdapter {
  readonly brokerId = 'pepperstone';
  readonly mode: BrokerMode;

  // Auth (OAuth tokens passed at construction)
  async authenticate(creds: BrokerCredentials): Promise<BrokerPingResult>;
  async ping(): Promise<BrokerPingResult>;

  // Account state
  async getAccount(): Promise<BrokerAccount>;
  async getPositions(): Promise<BrokerPosition[]>;
  async getOpenOrders(): Promise<BrokerOrder[]>;

  // Order management
  async submitOrder(order: BrokerOrderRequest): Promise<BrokerOrder>;
  async cancelOrder(brokerOrderId: string): Promise<void>;

  // Position closing
  async flatten(symbol?: string): Promise<void>;
  async closeAll(): Promise<void>;
}
```

### cTrader-Specific Considerations

1. **Symbol IDs**: cTrader uses numeric symbol IDs, not tickers. The adapter maintains a symbol cache.

2. **Volume units**: cTrader expresses volumes in "cents" (multiply lots × 100 × lot_size). Helper functions `volumeToLots()` and `lotsToVolume()` handle conversion.

3. **Timestamps**: cTrader uses milliseconds since epoch.

4. **REST vs WebSocket**: This implementation uses REST API. WebSocket streaming could be added for real-time fills.

---

## Security

### OAuth Security Measures

1. **PKCE (Proof Key for Code Exchange)**: Prevents authorization code interception attacks
   - Code verifier: 128-character random base64url string
   - Code challenge: SHA-256 hash of verifier

2. **State parameter**: Prevents CSRF attacks
   - Cryptographically random (via `secureId()`)
   - Single-use (marked `used_at` on consumption)
   - Short-lived (10 minute expiry)

3. **Token encryption**: AES-256-GCM encryption at rest
   - Same encryption used for API keys
   - Decryption only at adapter instantiation time

4. **Automatic refresh**: Tokens refreshed 48 hours before expiry
   - Prevents trading interruption
   - Logged to audit trail

### Audit Trail

All OAuth events logged to `broker_audit_log`:
- `oauth_connected`: Successful OAuth authorization
- `scheduled_token_refresh`: Token refresh job results

---

## Environment Variables

Required for Pepperstone integration:

```bash
# cTrader OAuth App Credentials (from openapi.ctrader.com)
CTRADER_CLIENT_ID=your_client_id
CTRADER_CLIENT_SECRET=your_client_secret

# Base URL for OAuth redirects
OAUTH_REDIRECT_BASE_URL=https://predict.agencio.cloud
```

---

## Deployment Checklist

### 1. Register cTrader OAuth Application

1. Go to https://openapi.ctrader.com/apps
2. Register a new application
3. Wait for status to change from "Submitted" to "Active"
4. Note the `client_id` and `client_secret`
5. Add redirect URL: `https://predict.agencio.cloud/api/predict/v1/user/brokers/pepperstone/oauth/callback`

### 2. Configure Environment

```bash
# Add to Vercel environment variables
CTRADER_CLIENT_ID=xxx
CTRADER_CLIENT_SECRET=xxx
OAUTH_REDIRECT_BASE_URL=https://predict.agencio.cloud
```

### 3. Apply Migration

```bash
# Apply migration 189 to production database
psql $DATABASE_URL < db/migrations/189_pepperstone_broker.sql
```

### 4. Enable Broker (Admin)

1. Go to `/admin/brokers`
2. Find Pepperstone in the list
3. Toggle "Enabled" to true
4. Optionally enable "Live Trading" if ready

---

## User Guide

### Connecting Pepperstone

1. Navigate to **Settings → Brokers**
2. Find **Pepperstone (cTrader)** in "Available to connect"
3. Click **Connect**
4. Review and acknowledge the compliance terms
5. Click **Connect with Pepperstone**
6. You'll be redirected to cTrader — log in with your Pepperstone credentials
7. Review the permissions and click **Authorize**
8. You'll be redirected back automatically
9. Your connection is now active — click **Test** to verify

### Supported Trading

- **Forex**: 70+ currency pairs (EUR/USD, GBP/USD, USD/JPY, etc.)
- **Indices**: S&P 500, NASDAQ, DAX, FTSE, Nikkei
- **Commodities**: Gold, Silver, Oil, Natural Gas
- **Crypto CFDs**: BTC, ETH (as CFDs, not spot)

### Paper vs Live

- **Demo accounts** connect via the demo endpoint
- **Live accounts** require admin-granted live trading role + MFA verification
- Mode is detected based on the cTrader account type after OAuth

---

## Troubleshooting

### "Broker not enabled on this platform"

An admin needs to enable Pepperstone at `/admin/brokers`.

### "Invalid or expired OAuth state"

The OAuth flow took too long (>10 minutes). Start over by clicking "Connect with Pepperstone" again.

### "Token exchange failed"

Check that `CTRADER_CLIENT_ID` and `CTRADER_CLIENT_SECRET` are correctly set and the OAuth app is in "Active" status.

### Connection works but orders fail

- Verify the cTrader account has trading permissions enabled
- Check that the symbol exists and market is open
- Review `broker_audit_log` for error details

### Token refresh failing

Check the scheduler logs. Common causes:
- Refresh token was revoked (user changed password)
- cTrader API is down
- Network connectivity issues

User may need to reconnect by clicking "Connect with Pepperstone" again.

---

## Related Documentation

- [13-broker-integration.md](./13-broker-integration.md) — General broker layer design
- [24-api-routes.md](./24-api-routes.md) — Full API route inventory
- [50-derivatives-and-shorts.md](./50-derivatives-and-shorts.md) — Perpetual swaps, futures, short selling
