diff --git a/README.md b/README.md
index 5abd619..5d81dcd 100644
--- a/README.md
+++ b/README.md
@@ -223,8 +223,12 @@ instance cannot log in by itself. Instead:
2. The extension relays the tab's short-lived access token to this dashboard
(`POST /api/vest/session`, held in memory only) every time the Vest app
renews it. If no Vest tab is open, the extension opens one in the background.
-3. On the dashboard, the Vest firm's credential is the **wallet address** of
- that login. The credential form offers the relayed wallet with one click.
+3. The Vest firm's credential on this dashboard is the **wallet address** of
+ that login, and it binds itself: the first relayed session fills it in when
+ the firm has none. A saved wallet is never replaced. If the tab is signed in
+ to a different login, the firm says so; remove its credential to switch.
+ With two logins relayed at once, nothing binds by itself, and the credential
+ form offers each relayed wallet with one click.
With no live session for the wallet, the client sits unauthenticated and says
so; it never tries to log in.
diff --git a/app/api/vest/session/route.ts b/app/api/vest/session/route.ts
index e0f89ba..8b5e76b 100644
--- a/app/api/vest/session/route.ts
+++ b/app/api/vest/session/route.ts
@@ -1,5 +1,6 @@
import { NextRequest } from 'next/server';
import { putVestSession, listVestSessions } from '@/lib/vest-session';
+import { autoBindVestWallet } from '@/lib/vest-wallet';
import { getTradableFirms } from '@/lib/firm-catalog';
import { getClients } from '@/lib/clients';
import { corsJson, corsPreflight } from '../../autobuyer/cors';
@@ -15,7 +16,10 @@ function wantedWallets(): string[] {
/**
* 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
+ * userId } — and it is held in memory for this machine's Vest client. A Vest
+ * firm here with no credential yet is bound to that login's wallet
+ * (lib/vest-wallet.ts). The extension posts again every minute, so a restart
+ * here is back on the session within a minute. 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.
@@ -38,6 +42,7 @@ export async function POST(req: NextRequest) {
});
if (refused) return corsJson({ ok: false, reason: refused }, { status: 400 });
+ const bound = autoBindVestWallet();
const wanted = wantedWallets();
const addr = String(body.primaryAddr).toLowerCase();
// A client waiting on this wallet's session syncs now, not at its next poll.
@@ -45,7 +50,7 @@ export async function POST(req: NextRequest) {
const vest = client as { onVestSession?: (wallet: string) => void };
vest.onVestSession?.(addr);
}
- return corsJson({ ok: true, used: wanted.includes(addr), wanted });
+ return corsJson({ ok: true, used: wanted.includes(addr), wanted, bound });
}
export async function GET() {
diff --git a/app/page.tsx b/app/page.tsx
index 775fe5b..b586e4f 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -209,9 +209,11 @@ function CredentialForm({ firm, onSaved }: { firm: FirmConfig; onSaved: () => vo
return ;
}
-/** A Vest firm's credential: the wallet its Vest login belongs to. The
- * extension has relayed the session of any Vest tab signed in on this
- * machine, so those wallets are offered with one click. */
+/** A Vest firm's credential: the wallet its Vest login belongs to. It binds
+ * itself as soon as the extension relays a Vest tab signed in on this
+ * machine (lib/vest-wallet.ts); this form is for when it can't — two logins
+ * relayed at once, or none yet — and offers the relayed wallets with one
+ * click. */
function VestWalletForm({ firm, onSaved }: { firm: FirmConfig; onSaved: () => void }) {
const [wallet, setWallet] = useState('');
const [seen, setSeen] = useState([]);
@@ -260,7 +262,7 @@ function VestWalletForm({ firm, onSaved }: { firm: FirmConfig; onSaved: () => vo
))}
0 ? 'or another wallet 0x…' : 'Vest wallet 0x… (sign in to Vest in Chrome first)'}
+ placeholder={seen.length > 0 ? 'or another wallet 0x…' : 'Sign in to Vest in Chrome: it binds itself (or paste 0x…)'}
value={wallet}
onChange={(e) => setWallet(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter' && valid) void save(wallet); }}
diff --git a/extension/README.md b/extension/README.md
index 6eb78b7..cbd9538 100644
--- a/extension/README.md
+++ b/extension/README.md
@@ -87,8 +87,9 @@ 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
+background.js relayVestSession lifts it every 10s, posts it when it changes (and every minute)
+dashboard POST /api/vest/session holds it in memory for the firm's Vest client,
+ and binds an unset Vest firm to the login's wallet
```
- The hook only reads the Vest app's own `/v3/auth/refresh` and `/register`
diff --git a/extension/background.js b/extension/background.js
index bd9f023..d446601 100644
--- a/extension/background.js
+++ b/extension/background.js
@@ -1208,11 +1208,15 @@ async function tick() {
const VEST_PATTERN = 'https://next.vestmarkets.com/*';
const VEST_URL = 'https://next.vestmarkets.com/portfolio';
const VEST_EVERY_MS = 10_000;
+/** The session is posted again this often even unchanged: the dashboard
+ * keeps it in memory only, so a restart there needs it back. */
+const VEST_REPOST_MS = 60_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 vestLastPostAt = 0;
let vestOpenedAt = 0;
/** Runs in the Vest tab's MAIN world: the session vest-relay.js holds, while
@@ -1276,7 +1280,7 @@ async function relayVestSession(cfg) {
}
const fingerprint = `${found.expiresAt}:${found.accessToken.length}:${found.accessToken.slice(-16)}`;
- if (fingerprint === vestLastRelayed) return;
+ if (fingerprint === vestLastRelayed && now - vestLastPostAt < VEST_REPOST_MS) return;
const res = await fetch(`${cfg.apiBase}/api/vest/session`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -1288,9 +1292,12 @@ async function relayVestSession(cfg) {
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;
+ vestLastPostAt = now;
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` });
+ const note = Array.isArray(body.bound) && body.bound.length > 0 ? ` · bound to ${body.bound.join(', ')}`
+ : body.used ? '' : ' (no Vest firm here uses this wallet)';
+ await setState({ vest: `Relayed ${short}${note} · good ${mins}m` });
}
// ── 2FA: the person's authenticator code for the firm site you are on ───────
diff --git a/extension/manifest.json b/extension/manifest.json
index 6a7f136..8f0bb76 100644
--- a/extension/manifest.json
+++ b/extension/manifest.json
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "AutoFirmer Capture",
- "version": "0.17.0",
+ "version": "0.17.1",
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxdbOMRR+58a9dHlbIKtlALtS3E4eltBGqfTnUc0lzJuWj5ZA4+5YFSdUnxv5N1ogxwpJUGkUV3MnKJuZnTekCmsNwjCy41a20Xtc7jQLHrIBZkesB6cB+d0l9Vo8M6EC3ZMD8Iv6MdAalU3GZh1Hn9vm24NjJ+xUT1qNIVHEcNUIb9evYobR/XHv+JAp6bOFnaLM+3wpyYKe4efMhsz0F86u9thRNhQnraTqUWfnKNpsHPK2zivaYHrziEEWX7Y3EsMHkf2FRhkUe7IDoxeJM49DBToTo9sAJUkcgLeGCGZLo7H3mcxyn/zERYR+rD3k5xdeBtjoZHXFx2+FanLb9wIDAQAB",
"update_url": "https://ext.juicerroom.com/updates.xml",
diff --git a/lib/vest-class.ts b/lib/vest-class.ts
index fe2208a..7df4007 100644
--- a/lib/vest-class.ts
+++ b/lib/vest-class.ts
@@ -49,7 +49,7 @@ import { firmInstruments } from './instruments';
import type { PlatformClient, PlatformAccount, PlatformContract, ClientHealth, PickContext } from './platform-client';
import { OrderNotPlacedError, isOrderNotPlaced, errorMessage } from './platform-client';
import { encodeContractName, decodeContractName, roundToStep, scoreCandidate } from './dxtrade-helpers';
-import { vestSessionFor, dropVestSession } from './vest-session';
+import { vestSessionFor, dropVestSession, listVestSessions } from './vest-session';
import {
vestAccountId, vestAccountName, vestDayOf, lastVestResetMs, parseCapitalAccount, parsePosition, parseOpenOrder,
parseTxn, bookTxns, awaitingLegs, unrealizedPnl, hourlyVolatility, marketOpen, num, toMs, formatDecimal,
@@ -231,7 +231,10 @@ export class VestClient implements PlatformClient {
private userToken(): string | null {
const session = vestSessionFor(this.wallet, Date.now(), USER_TOKEN_MIN_MS);
if (session) return session.accessToken;
- const msg = `no Vest session for ${this.wallet} — keep a Vest tab signed in to that wallet open in Chrome with the AutoFirmer extension`;
+ const other = listVestSessions().find((s) => s.primaryAddr !== this.wallet && s.expiresAt > Date.now());
+ const msg = other
+ ? `no Vest session for ${this.wallet} — the Vest tab here is signed in as ${other.primaryAddr}; sign in as ${this.wallet} there, or remove this firm's credential on the dashboard to switch to the tab's login`
+ : `no Vest session for ${this.wallet} — keep a Vest tab signed in to that wallet open in Chrome with the AutoFirmer extension`;
if (this.lastFetchErrors['login'] !== msg) console.warn(`${this.tag} ${msg}`);
this.lastAuthError = this.lastFetchErrors['login'] = msg;
return null;
diff --git a/lib/vest-wallet.ts b/lib/vest-wallet.ts
new file mode 100644
index 0000000..ff73b28
--- /dev/null
+++ b/lib/vest-wallet.ts
@@ -0,0 +1,42 @@
+/**
+ * Binds the Vest login the extension relays to this machine's Vest firm, so
+ * nobody has to copy a wallet address into the dashboard.
+ *
+ * A Vest firm's only credential here is the wallet of the login whose session
+ * it trades (lib/vest-session.ts). One person per machine, and Vest allows one
+ * profile per person, so the Vest tab signed in on this machine is the one to
+ * trade. It is bound automatically when:
+ * - the firm has no credential yet. A saved one is never replaced: someone
+ * chose it, and a different login in the tab is a mistake to flag (the
+ * client says so), not one to follow;
+ * - exactly one wallet has a live relayed session. Two logins in two tabs
+ * can't be told apart, so the dashboard's one-click form decides.
+ */
+
+import { getCachedCatalog } from './firm-catalog';
+import { getCredentials, setCredentials } from './db';
+import { listVestSessions } from './vest-session';
+import { reconcileClients } from './clients';
+import { scheduleBackupPush } from './backup-sync';
+
+/** Bind the relayed wallet to every Vest firm here that has no credential.
+ * Returns the names of the firms it bound. */
+export function autoBindVestWallet(now = Date.now()): string[] {
+ const live = [...new Set(listVestSessions().filter((s) => s.expiresAt > now).map((s) => s.primaryAddr))];
+ if (live.length !== 1) return [];
+ const wallet = live[0];
+ const bound: string[] = [];
+ for (const firm of getCachedCatalog()) {
+ if (firm.platform !== 'vest') continue;
+ const creds = getCredentials(firm.id);
+ if (creds && (creds.apiKey || creds.username || creds.password)) continue;
+ setCredentials(firm.id, firm.name, '', '', wallet);
+ bound.push(firm.name);
+ console.log(`[vest] ${firm.name}: bound to wallet ${wallet}, the Vest login signed in on this machine`);
+ }
+ if (bound.length > 0) {
+ reconcileClients();
+ scheduleBackupPush();
+ }
+ return bound;
+}