Files
Brandon LiandClaude Opus 5.5 96fdd9ca23 Vest platform: a client for Vest Markets, traded on a session the extension relays
Vest (next.vestmarkets.com) is a perps prop firm: index, metal and energy
perps on CME hours, plus stocks, FX and crypto. It joins as a fourth
platform, 'vest', on the DXtrade engine paths. Those are now keyed by
isNotionalPlatform() instead of === 'dxtrade': the firm's own symbols,
percent fees, notional sizing from volatility, and a scheduler lane of its
own.

- Login is relayed, not stored. Vest signs people in with Google through
  Privy and renews the session with a device key the browser won't export.
  Extension 0.17.0's vest-relay.js reads the token the Vest app renews for
  itself (~15 min). It renews nothing, so the tab is never logged out. The
  background script posts that token to POST /api/vest/session, which holds
  it in memory only. With no Vest tab open on a machine that trades Vest,
  the extension opens one.
- The firm's credential is the Vest login's wallet address. The dashboard's
  form offers the relayed wallet with one click.
- VestClient (lib/vest-class.ts), mapped from the web app's traffic and code
  since Vest publishes no API:
  - Reads the capital accounts, balances (free amount plus position
    collateral), positions and open orders with the user token.
  - Mints per-account tokens for orders and transactions.
  - Names accounts for their rules (V50K-E-D3-T10-34, V50K-F-D3-34,
    V25K-I-7) so account-type prefixes can match.
  - Takes Vest's own max-drawdown and daily-loss floors. dayStartBalance =
    floor + the type's limit, so the engine's room is never more than
    Vest's.
  - Uses each symbol's taker fee and margin rate from exchangeInfo, and
    hourly volatility over in-session hours only.
  - Places take-profit and stop as position legs, which Vest cancels as an
    OCO pair.
- Daily P&L and fund rows come from /v3/transactions by Vest's 20:00 ET
  day. A books check holds an account, naming the transaction types, when
  the balance and the transactions disagree.
- scripts/check-vest.mjs (in npm run check): Vest's day across DST, account
  names, parsing, volatility gaps, market hours, the session store.

Not yet run against Vest with a live session: the order and exit bodies
come from the app's code, and the transaction classification from its enum.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 14:19:31 -05:00

63 lines
2.9 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { getCachedFirm, hasPlatformCredentials } from '@/lib/firm-catalog';
import { setCredentials, clearCredentials } from '@/lib/db';
import { reconcileClients, removeClient } from '@/lib/clients';
import { scheduleBackupPush } from '@/lib/backup-sync';
type Params = { params: Promise<{ id: string }> };
/** The platform login for one firm. This is the only firm data this machine
* owns — everything else comes from the master catalog. The one path these
* credentials travel is the encrypted, bearer-gated backup to the master
* (lib/backup-sync.ts); they are still absent from the catalog and the
* control socket.
*
* Tradovate and DXTrade firms need username + password (DXTrade's server URL
* and login domain are set on the master, per firm). TopstepX firms need the
* API key; username is optional for now (the loginKey call still wants it,
* so the client sits idle with a clear error until one is added) and
* password is unused. Vest firms need the login's wallet address in the API
* key slot: Vest's session is relayed by the extension from a signed-in Vest
* tab (lib/vest-session.ts), and the address says whose to trade. */
export async function PUT(req: NextRequest, { params }: Params) {
const { id } = await params;
const firm = getCachedFirm(id);
if (!firm) return NextResponse.json({ error: 'Firm not found' }, { status: 404 });
const body = await req.json() as { username?: string; password?: string; apiKey?: string };
const username = body.username?.trim() ?? '';
const password = body.password?.trim() ?? '';
const apiKey = body.apiKey?.trim() ?? '';
if (!hasPlatformCredentials(firm.platform, { username, password, apiKey })) {
const error = firm.platform === 'topstepx'
? 'apiKey is required for a TopstepX firm'
: firm.platform === 'dxtrade'
? 'username and password are required for a DXtrade firm'
: firm.platform === 'vest'
? 'a Vest firm needs the wallet address of the Vest login (0x followed by 40 hex characters)'
: 'username and password are required';
return NextResponse.json({ error }, { status: 400 });
}
try {
setCredentials(id, firm.name, username, password, apiKey);
// Picks up the new login immediately rather than at the next restart.
reconcileClients();
scheduleBackupPush();
return NextResponse.json({ ok: true });
} catch (err) {
console.error('[PUT /api/firms/:id/credentials]', err);
return NextResponse.json({ error: 'Failed to save credentials' }, { status: 500 });
}
}
export async function DELETE(_req: NextRequest, { params }: Params) {
const { id } = await params;
const cleared = clearCredentials(id);
removeClient(id);
scheduleBackupPush();
return NextResponse.json({ ok: cleared });
}