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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5ec5abde6e
commit
96fdd9ca23
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Vest sessions relayed by the extension.
|
||||
*
|
||||
* Vest logs a person in through Google/Privy in the browser, and refreshes
|
||||
* the session with a device key the browser will not export, so this
|
||||
* machine cannot log in by itself. Instead the AutoFirmer extension watches
|
||||
* the signed-in Vest tab: whenever the Vest app renews its own ~15-minute
|
||||
* access token, the extension posts it here (POST /api/vest/session). The
|
||||
* Vest client (lib/vest-class.ts) trades with the latest one for its wallet
|
||||
* and mints per-account tokens from it.
|
||||
*
|
||||
* Kept in memory only — never in the DB, the logs, a report or a backup. A
|
||||
* restart forgets them; the Vest tab's next renewal (or the extension's
|
||||
* next tick) relays a fresh one within minutes.
|
||||
*/
|
||||
|
||||
export interface VestSession {
|
||||
accessToken: string;
|
||||
/** Epoch ms the access token stops working. */
|
||||
expiresAt: number;
|
||||
/** The wallet the Vest login belongs to, lower-case. */
|
||||
primaryAddr: string;
|
||||
userId: string;
|
||||
receivedAt: number;
|
||||
}
|
||||
|
||||
/** What the dashboard and the extension may see of a session: never the token. */
|
||||
export interface VestSessionInfo {
|
||||
primaryAddr: string;
|
||||
userId: string;
|
||||
expiresAt: number;
|
||||
receivedAt: number;
|
||||
}
|
||||
|
||||
const g = globalThis as typeof globalThis & { __vestSessions?: Map<string, VestSession> };
|
||||
|
||||
function store(): Map<string, VestSession> {
|
||||
return (g.__vestSessions ??= new Map());
|
||||
}
|
||||
|
||||
/** Shortest life a relayed token must have left to be taken. */
|
||||
const MIN_REMAINING_MS = 30_000;
|
||||
/** Longest a relayed token may claim to live — a Vest access token is
|
||||
* ~15 minutes; anything far beyond that is not one. */
|
||||
const MAX_REMAINING_MS = 6 * 60 * 60_000;
|
||||
|
||||
/** Take a relayed session. Returns why it was refused, or null. A token for
|
||||
* a wallet only ever replaces one that expires sooner. */
|
||||
export function putVestSession(input: { accessToken: unknown; expiresAt: unknown; primaryAddr: unknown; userId: unknown }, now = Date.now()): string | null {
|
||||
const token = typeof input.accessToken === 'string' ? input.accessToken.trim() : '';
|
||||
const expiresAt = Number(input.expiresAt);
|
||||
const addr = typeof input.primaryAddr === 'string' ? input.primaryAddr.trim().toLowerCase() : '';
|
||||
if (!/^[\w-]+\.[\w-]+\.[\w-]+$/.test(token) || token.length > 8192) return 'accessToken is not a JWT';
|
||||
if (!/^0x[0-9a-f]{40}$/.test(addr)) return 'primaryAddr is not a wallet address';
|
||||
if (!Number.isFinite(expiresAt) || expiresAt - now < MIN_REMAINING_MS) return 'token already expired';
|
||||
if (expiresAt - now > MAX_REMAINING_MS) return 'expiresAt is implausibly far away';
|
||||
const prev = store().get(addr);
|
||||
if (prev && prev.expiresAt > expiresAt && prev.accessToken !== token) return null; // keep the fresher one
|
||||
store().set(addr, {
|
||||
accessToken: token,
|
||||
expiresAt,
|
||||
primaryAddr: addr,
|
||||
userId: typeof input.userId === 'string' ? input.userId : '',
|
||||
receivedAt: now,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The live session for a wallet, or null when none has been relayed or it
|
||||
* is about to expire. */
|
||||
export function vestSessionFor(primaryAddr: string, now = Date.now(), minRemainingMs = MIN_REMAINING_MS): VestSession | null {
|
||||
const s = store().get(primaryAddr.trim().toLowerCase());
|
||||
return s && s.expiresAt - now >= minRemainingMs ? s : null;
|
||||
}
|
||||
|
||||
/** Drop a wallet's session — the gateway refused it. */
|
||||
export function dropVestSession(primaryAddr: string, token: string): void {
|
||||
const key = primaryAddr.trim().toLowerCase();
|
||||
if (store().get(key)?.accessToken === token) store().delete(key);
|
||||
}
|
||||
|
||||
/** Every relayed session, newest first, without tokens. */
|
||||
export function listVestSessions(): VestSessionInfo[] {
|
||||
return [...store().values()]
|
||||
.sort((a, b) => b.receivedAt - a.receivedAt)
|
||||
.map(({ primaryAddr, userId, expiresAt, receivedAt }) => ({ primaryAddr, userId, expiresAt, receivedAt }));
|
||||
}
|
||||
Reference in New Issue
Block a user