Vest: the bot activates a passed evaluation's funded account, and a passed one no longer holds the sync

A passed Vest evaluation sits at GOAL_PASSED (status 3) until someone presses
"Go to funded account", which sends POST /v3/capital/activate-funded
{eval_account_id} on the evaluation's own token (read from the Vest app's
bundle, 2026-10-29); Vest then opens the funded account and marks the
evaluation CLAIMED (6). The 5 evaluations that passed on 2026-10-29 waited
for a hand activation.

runActivations now asks for it, the way runClaims claims: from the
scheduler's tick (not awaited, so a slow ask doesn't hold the lane's entry)
and from the claim watcher while the scheduler is off, for evaluations with
an account type only, 15 minutes apart after a failure. Off with the
auto_activate setting at 'off' (dashboard: "Auto-activate funded"). The
client asks for an evaluation once at a time, never again after Vest
answered, and after a timeout not until a list read a minute on says either
way; a refusal frees it at once. The rule is the Vest app's own: an
evaluation at GOAL_PASSED, not blocked, on its plan's last step.

Vest's active balance list leaves a passed evaluation out, and a missing
balance made every sync short of good (15:31-15:34Z), which holds and then
quarantines the lane. Only a live account's missing balance does now. Log
lines name Vest's statuses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-10-29 12:09:49 -05:00
co-authored by Claude Opus 5.5
parent 0eb56b3949
commit 8a5b2a64e7
8 changed files with 380 additions and 18 deletions
+51 -4
View File
@@ -211,7 +211,15 @@ export interface VestCapitalAccount {
productType: string;
/** 1-based stage; stages[stage-1] is the current one. */
stage: number;
/** Evaluation steps the plan has (a 1-Step's is 1, a 2-Step's 2). */
stepsTotal: number;
/** The evaluation step this account is (1-based; a funded one's is
* stepsTotal + 1, as the Vest app counts it when Vest leaves it out). */
step: number;
/** Vest's account_type: 2 an evaluation, 3 a funded account. */
accountType: number;
/** Vest blocked the account (blocked_at set): nothing it offers applies. */
blocked: boolean;
/** The current stage: 'eval' or 'funded'. */
kind: string;
status: number;
@@ -236,13 +244,18 @@ export function parseCapitalAccount(raw: unknown): VestCapitalAccount | null {
const stages = Array.isArray(a.stages) ? (a.stages as Record<string, unknown>[]) : [];
const stage = num(a.stage) ?? 1;
const current = stages.find((s) => num(s.ordinal) === stage) ?? stages[stage - 1] ?? {};
// The Vest app's own fallbacks, for an answer that leaves these out.
const stepsTotal = num(a.steps_total) ?? (a.plan_product_type === 'eval' ? 1 : 0);
return {
id: a.id,
planId: typeof a.plan_id === 'string' ? a.plan_id : '',
planName: typeof a.plan_name === 'string' ? a.plan_name : '',
productType: typeof a.plan_product_type === 'string' ? a.plan_product_type : '',
stage,
stepsTotal: num(a.steps_total) ?? 0,
stepsTotal,
step: num(a.step) ?? (stage === VEST_STAGE_FUNDED ? stepsTotal + 1 : 1),
accountType: num(a.account_type) ?? 0,
blocked: Boolean(a.blocked_at),
kind: typeof current.kind === 'string' ? current.kind : '',
status: num(a.status) ?? -1,
attemptIndex: num(a.attempt_index) ?? 0,
@@ -259,9 +272,43 @@ export function parseCapitalAccount(raw: unknown): VestCapitalAccount | null {
};
}
/** The capital account status every listed live account showed (2026-10-26).
* Anything else is treated as unable to trade until seen and named. */
export const VEST_STATUS_ACTIVE = 2;
/** Vest's capital account statuses, as the Vest app names them (its bundle,
* 2026-10-29). Only an ACTIVE account trades. A passed evaluation sits at
* GOAL_PASSED until its funded account is activated, and is CLAIMED after. */
export const VEST_STATUS = {
CREATED: 1,
ACTIVE: 2,
GOAL_PASSED: 3,
FAILED_MAX_DRAWDOWN: 4,
FAILED_MAX_DAILY_LOSS: 5,
CLAIMED: 6,
EXPIRED: 7,
} as const;
export const VEST_STATUS_ACTIVE = VEST_STATUS.ACTIVE;
/** Vest's stage field: an evaluation's, then the funded account's. */
const VEST_STAGE_EVAL = 1;
const VEST_STAGE_FUNDED = 2;
/** Vest's account_type of an evaluation. */
const VEST_ACCOUNT_TYPE_EVAL = 2;
/** A status as the log says it: Vest's own name, with its number. */
export function vestStatusName(status: number): string {
const name = Object.entries(VEST_STATUS).find(([, v]) => v === status)?.[0];
return `${name ? name.toLowerCase().replace(/_/g, ' ') : 'unknown'} (status ${status})`;
}
/**
* A passed evaluation whose funded account Vest activates on request (POST
* /v3/capital/activate-funded) — the Vest app's own test for its "Go to
* funded account" button: an evaluation at GOAL_PASSED, not blocked, on its
* plan's last step. An earlier step of a 2-Step passes on to the next step
* by itself.
*/
export function awaitingActivation(a: VestCapitalAccount): boolean {
const evaluation = a.accountType === VEST_ACCOUNT_TYPE_EVAL || a.stage === VEST_STAGE_EVAL;
return evaluation && a.status === VEST_STATUS.GOAL_PASSED && !a.blocked && a.stepsTotal > 0 && a.step === a.stepsTotal;
}
const pct = (fraction: number) => Math.round(fraction * 1000) / 10;