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>
69 lines
3.0 KiB
JavaScript
69 lines
3.0 KiB
JavaScript
/* AutoFirmer — Vest session hook.
|
|
*
|
|
* Runs in the page's MAIN world on next.vestmarkets.com, at document_start
|
|
* (manifest content script) or injected later into a tab that was already open
|
|
* (background.js relayVestSession). Installs once per page.
|
|
*
|
|
* Vest's app renews its own ~15-minute access token by itself: at load, then
|
|
* whenever one nears expiry (POST /v3/auth/refresh, or /v3/auth/register at
|
|
* sign-in). Those answers carry the token, its expiry and the login's wallet.
|
|
* This hook reads them off the app's own XHRs and keeps the latest on
|
|
* `window.__afVestSession`, where background.js collects it and relays it to
|
|
* the local dashboard, whose Vest client trades with it.
|
|
*
|
|
* It sends nothing anywhere, changes no request and never renews anything
|
|
* itself: the session stays the app's, refreshed by the app with its own
|
|
* device key, so the tab is never logged out by the bot.
|
|
*/
|
|
|
|
(() => {
|
|
if (window.__afVestHook) return;
|
|
window.__afVestHook = true;
|
|
|
|
const AUTH = /^https:\/\/api-gateway\.hz\.vestmarkets\.com\/v3\/auth\/(refresh|register)(\?|$)/;
|
|
|
|
function take(url, status, text) {
|
|
if (status < 200 || status >= 300 || !AUTH.test(url)) return;
|
|
let body;
|
|
try { body = JSON.parse(text); } catch { return; }
|
|
if (!body || typeof body.accessToken !== 'string' || !body.accessToken) return;
|
|
const expiresAt = Number(body.accessExpiresAtMs);
|
|
if (!Number.isFinite(expiresAt)) return;
|
|
window.__afVestSession = {
|
|
accessToken: body.accessToken,
|
|
expiresAt,
|
|
primaryAddr: typeof body.primaryAddr === 'string' ? body.primaryAddr : '',
|
|
userId: typeof body.userId === 'string' ? body.userId : '',
|
|
at: Date.now(),
|
|
};
|
|
}
|
|
|
|
const open = XMLHttpRequest.prototype.open;
|
|
const send = XMLHttpRequest.prototype.send;
|
|
XMLHttpRequest.prototype.open = function (method, url) {
|
|
try { this.__afVestUrl = String(url); } catch { /* ignore */ }
|
|
return open.apply(this, arguments);
|
|
};
|
|
XMLHttpRequest.prototype.send = function () {
|
|
if (this.__afVestUrl && AUTH.test(this.__afVestUrl)) {
|
|
this.addEventListener('loadend', () => {
|
|
try {
|
|
if (this.responseType === '' || this.responseType === 'text') take(this.__afVestUrl, this.status, this.responseText);
|
|
else if (this.responseType === 'json') take(this.__afVestUrl, this.status, JSON.stringify(this.response));
|
|
} catch { /* never break the app */ }
|
|
});
|
|
}
|
|
return send.apply(this, arguments);
|
|
};
|
|
|
|
const fetch0 = window.fetch;
|
|
window.fetch = async function (input) {
|
|
const res = await fetch0.apply(this, arguments);
|
|
try {
|
|
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input && input.url;
|
|
if (url && AUTH.test(url)) res.clone().text().then((t) => take(url, res.status, t)).catch(() => {});
|
|
} catch { /* never break the app */ }
|
|
return res;
|
|
};
|
|
})();
|