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
@@ -79,6 +79,31 @@ A new 2FA site needs its host in `host_permissions` (not the captcha
|
||||
`content_scripts` list), a Site link on its firm on the master, and its codes
|
||||
assigned on the master's Codes page.
|
||||
|
||||
## Vest session relay
|
||||
|
||||
Vest signs people in with Google (through Privy) in the browser and renews the
|
||||
session with a device key the browser will not export, so the dashboard cannot
|
||||
log in to Vest by itself. The extension relays the browser's session instead:
|
||||
|
||||
```
|
||||
vest-relay.js (Vest tab, MAIN world) keeps the access token the Vest app last renewed
|
||||
background.js relayVestSession lifts it every 10s, posts it when it changes
|
||||
dashboard POST /api/vest/session holds it in memory for the firm's Vest client
|
||||
```
|
||||
|
||||
- The hook only reads the Vest app's own `/v3/auth/refresh` and `/register`
|
||||
answers. It renews nothing and sends nothing; the session stays the app's,
|
||||
so the bot never logs the tab out.
|
||||
- The token lives ~15 minutes and the app renews it by itself while the tab is
|
||||
open. The tab is marked not auto-discardable, since a discarded tab stops
|
||||
renewing.
|
||||
- When the dashboard has a Vest firm (its credential is the login's wallet)
|
||||
and no Vest tab is open, the extension opens one in the background. The app
|
||||
signs back in from its 30-day session. After that the person signs in again
|
||||
in that tab.
|
||||
- This is the one place the extension reads a tab you are not looking at. It
|
||||
only reads `next.vestmarkets.com` tabs, and only that one value.
|
||||
|
||||
## Notes
|
||||
|
||||
- The extension never captures the dashboard's own pages — otherwise it would
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
* ready at all times; a run happens because someone asked for one.
|
||||
*
|
||||
* On a firm site, the person's 2FA code for it shows in a corner chip.
|
||||
*
|
||||
* A signed-in Vest tab's session is relayed to the dashboard, whose Vest client
|
||||
* trades with it (relayVestSession).
|
||||
*/
|
||||
|
||||
const DEFAULTS = {
|
||||
@@ -1185,6 +1188,109 @@ async function tick() {
|
||||
} catch (err) {
|
||||
await setState({ connected: true, error: `2FA chip: ${err.message}` });
|
||||
}
|
||||
|
||||
try {
|
||||
await relayVestSession(cfg);
|
||||
} catch (err) {
|
||||
await setState({ connected: true, vest: `Vest relay: ${err.message}` });
|
||||
}
|
||||
}
|
||||
|
||||
// ── Vest: relay the signed-in tab's session to the dashboard ────────────────
|
||||
//
|
||||
// Vest signs people in through Google in the browser and renews the session
|
||||
// with a device key the browser won't hand out, so the dashboard cannot log in
|
||||
// by itself. vest-relay.js, in the Vest tab, keeps the latest access token the
|
||||
// Vest app renewed for itself (~15 minutes each); this lifts it and posts it to
|
||||
// the dashboard whenever it changes. Nothing here renews or signs anything: the
|
||||
// session stays the app's.
|
||||
|
||||
const VEST_PATTERN = 'https://next.vestmarkets.com/*';
|
||||
const VEST_URL = 'https://next.vestmarkets.com/portfolio';
|
||||
const VEST_EVERY_MS = 10_000;
|
||||
/** A Vest tab opened for the relay isn't opened again for this long. */
|
||||
const VEST_REOPEN_MS = 10 * 60_000;
|
||||
|
||||
let vestLastCheck = 0;
|
||||
let vestLastRelayed = '';
|
||||
let vestOpenedAt = 0;
|
||||
|
||||
/** Runs in the Vest tab's MAIN world: the session vest-relay.js holds, while
|
||||
* it has life left. Self-contained. */
|
||||
function pageVestSession() {
|
||||
const s = window.__afVestSession;
|
||||
return { hooked: window.__afVestHook === true, session: s && s.expiresAt > Date.now() + 30000 ? s : null };
|
||||
}
|
||||
|
||||
/** The wallets the dashboard's Vest firms trade; [] from an older dashboard. */
|
||||
async function fetchVestWanted(cfg) {
|
||||
try {
|
||||
const res = await fetch(`${cfg.apiBase}/api/vest/session`, { cache: 'no-store' });
|
||||
if (!res.ok) return [];
|
||||
const body = await res.json();
|
||||
return Array.isArray(body.wanted) ? body.wanted : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function relayVestSession(cfg) {
|
||||
const now = Date.now();
|
||||
if (now - vestLastCheck < VEST_EVERY_MS) return;
|
||||
vestLastCheck = now;
|
||||
|
||||
const tabs = (await chrome.tabs.query({ url: VEST_PATTERN })).filter((t) => /^https:/.test(t.url || ''));
|
||||
if (tabs.length === 0) {
|
||||
// This machine trades Vest: open the app in the background. It signs
|
||||
// itself back in from its 30-day session, and the hook catches that.
|
||||
const wanted = await fetchVestWanted(cfg);
|
||||
if (wanted.length === 0) return;
|
||||
if (now - vestOpenedAt > VEST_REOPEN_MS) {
|
||||
vestOpenedAt = now;
|
||||
await chrome.tabs.create({ url: VEST_URL, active: false });
|
||||
await setState({ vest: 'Opened a Vest tab to relay its session' });
|
||||
} else {
|
||||
await setState({ vest: 'No Vest tab open' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let found = null;
|
||||
for (const tab of tabs) {
|
||||
// A discarded tab stops renewing the session; keep Chrome from it.
|
||||
if (tab.autoDiscardable !== false) chrome.tabs.update(tab.id, { autoDiscardable: false }).catch(() => {});
|
||||
const [injection] = await chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id }, world: 'MAIN', func: pageVestSession,
|
||||
}).catch(() => [null]);
|
||||
const out = injection?.result;
|
||||
if (out && !out.hooked) {
|
||||
// A tab open from before this build: hook it now, and its app's
|
||||
// next renewal is caught.
|
||||
await chrome.scripting.executeScript({ target: { tabId: tab.id }, world: 'MAIN', files: ['vest-relay.js'] }).catch(() => {});
|
||||
}
|
||||
if (out?.session) { found = out.session; break; }
|
||||
}
|
||||
if (!found) {
|
||||
await setState({ vest: 'Vest tab open — waiting for its session (sign in there if it shows Log In)' });
|
||||
return;
|
||||
}
|
||||
|
||||
const fingerprint = `${found.expiresAt}:${found.accessToken.length}:${found.accessToken.slice(-16)}`;
|
||||
if (fingerprint === vestLastRelayed) return;
|
||||
const res = await fetch(`${cfg.apiBase}/api/vest/session`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
accessToken: found.accessToken, expiresAt: found.expiresAt, primaryAddr: found.primaryAddr, userId: found.userId,
|
||||
}),
|
||||
});
|
||||
const body = await res.json().catch(() => ({}));
|
||||
if (res.status === 404) throw new Error('this dashboard is too old for the Vest relay');
|
||||
if (!res.ok) throw new Error(`dashboard refused the session: ${body.reason || body.error || `HTTP ${res.status}`}`);
|
||||
vestLastRelayed = fingerprint;
|
||||
const short = `${found.primaryAddr.slice(0, 6)}…${found.primaryAddr.slice(-4)}`;
|
||||
const mins = Math.max(0, Math.round((found.expiresAt - Date.now()) / 60000));
|
||||
await setState({ vest: `Relayed ${short}${body.used ? '' : ' (no Vest firm here uses this wallet)'} · good ${mins}m` });
|
||||
}
|
||||
|
||||
// ── 2FA: the person's authenticator code for the firm site you are on ───────
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "AutoFirmer Capture",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
|
||||
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxdbOMRR+58a9dHlbIKtlALtS3E4eltBGqfTnUc0lzJuWj5ZA4+5YFSdUnxv5N1ogxwpJUGkUV3MnKJuZnTekCmsNwjCy41a20Xtc7jQLHrIBZkesB6cB+d0l9Vo8M6EC3ZMD8Iv6MdAalU3GZh1Hn9vm24NjJ+xUT1qNIVHEcNUIb9evYobR/XHv+JAp6bOFnaLM+3wpyYKe4efMhsz0F86u9thRNhQnraTqUWfnKNpsHPK2zivaYHrziEEWX7Y3EsMHkf2FRhkUe7IDoxeJM49DBToTo9sAJUkcgLeGCGZLo7H3mcxyn/zERYR+rD3k5xdeBtjoZHXFx2+FanLb9wIDAQAB",
|
||||
"update_url": "https://ext.juicerroom.com/updates.xml",
|
||||
|
||||
"description": "Scrapes the HTML of the firm tab you are viewing and posts it to the AutoFirmer dashboard, and shows the 2FA code for the firm site you are on.",
|
||||
"description": "Scrapes the HTML of the firm tab you are viewing and posts it to the AutoFirmer dashboard, shows the 2FA code for the firm site you are on, and relays a signed-in Vest tab's session to the dashboard.",
|
||||
"permissions": ["scripting", "tabs", "storage", "alarms"],
|
||||
"host_permissions": [
|
||||
"https://*.tradeify.co/*",
|
||||
"https://*.lucidtrading.com/*",
|
||||
"https://*.topstep.com/*",
|
||||
"https://*.apextraderfunding.com/*",
|
||||
"https://*.vestmarkets.com/*",
|
||||
"http://localhost/*",
|
||||
"http://127.0.0.1/*"
|
||||
],
|
||||
@@ -29,6 +30,12 @@
|
||||
"run_at": "document_start",
|
||||
"all_frames": true,
|
||||
"world": "MAIN"
|
||||
},
|
||||
{
|
||||
"matches": ["https://next.vestmarkets.com/*"],
|
||||
"js": ["vest-relay.js"],
|
||||
"run_at": "document_start",
|
||||
"world": "MAIN"
|
||||
}
|
||||
],
|
||||
"action": {
|
||||
|
||||
@@ -29,6 +29,7 @@ async function render() {
|
||||
document.getElementById('status').innerHTML =
|
||||
`<span class="dot" style="background:${color}"></span><strong>${label}</strong>` +
|
||||
detail +
|
||||
(state.vest ? `<div class="muted">Vest: ${esc(state.vest)}</div>` : '') +
|
||||
(state.error ? `<div class="err">${esc(state.error)}</div>` : '');
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/* 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;
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user