Files
autofirmer-expanded/app/api/vest/session/route.ts
T
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

58 lines
2.2 KiB
TypeScript

import { NextRequest } from 'next/server';
import { putVestSession, listVestSessions } from '@/lib/vest-session';
import { getTradableFirms } from '@/lib/firm-catalog';
import { getClients } from '@/lib/clients';
import { corsJson, corsPreflight } from '../../autobuyer/cors';
/** The wallets this machine's Vest firms trade (their saved credential), lower-case. */
function wantedWallets(): string[] {
const wallets = getTradableFirms()
.filter((f) => f.platform === 'vest')
.map((f) => f.apiKey.trim().toLowerCase());
return [...new Set(wallets)];
}
/**
* The extension's Vest relay (lib/vest-session.ts): POST the access token the
* signed-in Vest tab just renewed — { accessToken, expiresAt, primaryAddr,
* userId } — and it is held in memory for this machine's Vest client. GET
* lists what has been relayed (never the tokens) and which wallets this
* machine wants, so the extension knows whether to keep a Vest tab open and
* the dashboard can offer the address as the firm's credential.
*
* Same LAN-only, any-origin CORS as the AutoBuyer routes: the extension's
* service worker calls it.
*/
export async function POST(req: NextRequest) {
let body: Record<string, unknown>;
try {
body = await req.json() as Record<string, unknown>;
} catch {
return corsJson({ error: 'JSON body required' }, { status: 400 });
}
const refused = putVestSession({
accessToken: body.accessToken,
expiresAt: body.expiresAt,
primaryAddr: body.primaryAddr,
userId: body.userId,
});
if (refused) return corsJson({ ok: false, reason: refused }, { status: 400 });
const wanted = wantedWallets();
const addr = String(body.primaryAddr).toLowerCase();
// A client waiting on this wallet's session syncs now, not at its next poll.
for (const client of getClients().values()) {
const vest = client as { onVestSession?: (wallet: string) => void };
vest.onVestSession?.(addr);
}
return corsJson({ ok: true, used: wanted.includes(addr), wanted });
}
export async function GET() {
return corsJson({ sessions: listVestSessions(), wanted: wantedWallets() });
}
export async function OPTIONS() {
return corsPreflight();
}