dexterm-cli 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/packages/autotrade/src/chat/connector.js +5 -4
- package/dist/packages/autotrade/src/chat/control.js +3 -0
- package/dist/packages/autotrade/src/chat/hub.js +37 -2
- package/dist/packages/autotrade/src/chat/index.js +3 -2
- package/dist/packages/autotrade/src/chat/readonly.js +530 -0
- package/dist/packages/autotrade/src/chat/service.js +12 -1
- package/dist/packages/autotrade/src/engine.js +75 -29
- package/dist/packages/autotrade/src/index.js +2 -0
- package/dist/packages/autotrade/src/runner.js +72 -14
- package/dist/packages/autotrade/src/store.js +1 -1
- package/dist/packages/autotrade/src/strategy.js +17 -0
- package/dist/packages/autotrade/src/tiers.js +194 -0
- package/dist/packages/classifier/src/config.js +90 -12
- package/dist/packages/classifier/src/index.js +1 -0
- package/dist/packages/classifier/src/win-acl.js +71 -0
- package/dist/packages/cli/src/app-help-gen.js +1 -1
- package/dist/packages/cli/src/autotrade-cmd/fleet.js +4 -2
- package/dist/packages/cli/src/autotrade-cmd/run.js +4 -2
- package/dist/packages/cli/src/autotrade-cmd/service.js +3 -1
- package/dist/packages/cli/src/autotrade-cmd/strategies.js +1 -1
- package/dist/packages/cli/src/autotrade-cmd/telegram-host.js +2 -0
- package/dist/packages/cli/src/classifier-cmd.js +16 -8
- package/dist/packages/cli/src/skill-eval-build.js +7 -2
- package/dist/packages/cli/src/version.js +1 -1
- package/dist/packages/mcp/assets/kb/app-help/autotrade-telegram.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-confidence-tiers-experimental-unproven-paper-only.md +56 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-hosted-jev-with-your-own-key.md +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-where-to-find-it.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-read-only-data-check-top-and-signals-experimental.md +48 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-will-not-do.md +1 -1
- package/dist/packages/mcp/src/server.js +2 -1
- package/dist/packages/packs/src/skill-care.js +10 -2
- package/package.json +1 -1
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// ~/.dexterm/classifier.json (schema dexterm.classifier/1). `provider` is the one switch (`classifier.provider`);
|
|
2
2
|
// `default` is the shipped recommendation (`classifier.default`), chosen by the task-1822 benchmark. Loading never
|
|
3
3
|
// throws: each bad field falls back to its default and is reported. Writing is atomic, 0600 in a 0700 dir.
|
|
4
|
-
// The hosted API key is NOT stored here: it lives in its own 0600 file (
|
|
4
|
+
// The hosted API key is NOT stored here: it lives in its own 0600 file (on Windows an owner-only ACL, see
|
|
5
|
+
// win-acl.ts) or the TYPESAFE_API_KEY env var.
|
|
5
6
|
import fs from 'node:fs';
|
|
6
7
|
import { homedir } from 'node:os';
|
|
7
8
|
import path from 'node:path';
|
|
8
9
|
import { CATALOG } from "./models.js";
|
|
9
10
|
import { PROVIDER_IDS } from "./types.js";
|
|
11
|
+
import { checkOwnerOnly, currentUserSid, lockOwnerOnly } from "./win-acl.js";
|
|
10
12
|
export const CLASSIFIER_SCHEMA = 'dexterm.classifier/1';
|
|
11
13
|
/**
|
|
12
14
|
* The shipped default (task-1822 benchmark, research/classifier-2026-10/REPORT.md §3, picked on VALID before any
|
|
@@ -163,32 +165,108 @@ export function saveClassifierConfig(c, env = process.env) {
|
|
|
163
165
|
writeAtomic(p, JSON.stringify(c, null, 2) + '\n');
|
|
164
166
|
return p;
|
|
165
167
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
+
// A Windows ACL probe spawns PowerShell (~0.5 s) and hosted calls read the key per request, so a passing check is
|
|
169
|
+
// remembered for one minute per (path, size, mtime, ctime).
|
|
170
|
+
const aclOk = new Map();
|
|
171
|
+
const ACL_OK_MS = 60_000;
|
|
172
|
+
/** Where the TypeSafe key comes from, or why the key file is refused. The key itself is never logged. */
|
|
173
|
+
export function typesafeKeyState(env = process.env, opts = {}) {
|
|
168
174
|
const e = env.TYPESAFE_API_KEY?.trim();
|
|
169
175
|
if (e)
|
|
170
|
-
return e;
|
|
176
|
+
return { source: 'env', key: e };
|
|
177
|
+
const p = typesafeKeyPath(env);
|
|
178
|
+
let st;
|
|
171
179
|
try {
|
|
172
|
-
|
|
173
|
-
const st = fs.statSync(p);
|
|
174
|
-
if ((st.mode & 0o077) !== 0)
|
|
175
|
-
return null; // refuse a key file other users can read
|
|
176
|
-
const k = fs.readFileSync(p, 'utf8').trim();
|
|
177
|
-
return k || null;
|
|
180
|
+
st = fs.statSync(p);
|
|
178
181
|
}
|
|
179
182
|
catch {
|
|
180
|
-
return
|
|
183
|
+
return { source: 'none', path: p };
|
|
184
|
+
}
|
|
185
|
+
if ((opts.platform ?? process.platform) === 'win32') {
|
|
186
|
+
// NTFS has no mode bits (stat says 0666 for every file): the access list decides
|
|
187
|
+
const memo = `${p}|${st.size}|${st.mtimeMs}|${st.ctimeMs}`;
|
|
188
|
+
if (opts.fresh || opts.acl || !((aclOk.get(memo) ?? 0) > Date.now())) {
|
|
189
|
+
aclOk.delete(memo);
|
|
190
|
+
const c = checkOwnerOnly(p, opts.acl);
|
|
191
|
+
if (!c.ok)
|
|
192
|
+
return { source: 'refused', path: p, reason: c.reason };
|
|
193
|
+
if (!opts.acl)
|
|
194
|
+
aclOk.set(memo, Date.now() + ACL_OK_MS); // injected (test) probes are never remembered
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else if ((st.mode & 0o077) !== 0) {
|
|
198
|
+
// refuse a key file other users can read
|
|
199
|
+
return { source: 'refused', path: p, reason: `other users can access it (mode ${(st.mode & 0o777).toString(8)}; chmod 600 it)` };
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
const k = fs.readFileSync(p, 'utf8').trim();
|
|
203
|
+
return k ? { source: 'file', key: k, path: p } : { source: 'none', path: p };
|
|
204
|
+
}
|
|
205
|
+
catch (x) {
|
|
206
|
+
return { source: 'refused', path: p, reason: `it cannot be read (${x.code ?? 'error'})` };
|
|
181
207
|
}
|
|
182
208
|
}
|
|
183
|
-
|
|
209
|
+
/** One line on why a key file is refused, naming the env var as the way out. Never contains the key. */
|
|
210
|
+
export const typesafeKeyRefusal = (s) => `the TypeSafe key file ${s.path} is refused: ${s.reason}. Run \`dexterm classifier key set\` again, or set TYPESAFE_API_KEY in your environment instead.`;
|
|
211
|
+
/** The user's TypeSafe key: env TYPESAFE_API_KEY first, then the owner-only key file (0600; on Windows an owner-only ACL). Never logged. */
|
|
212
|
+
export function readTypesafeKey(env = process.env, opts = {}) {
|
|
213
|
+
const s = typesafeKeyState(env, opts);
|
|
214
|
+
return s.source === 'env' || s.source === 'file' ? s.key : null;
|
|
215
|
+
}
|
|
216
|
+
export function writeTypesafeKey(key, env = process.env, opts = {}) {
|
|
184
217
|
const k = key.trim();
|
|
185
218
|
if (!/^[\x21-\x7e]{8,512}$/.test(k))
|
|
186
219
|
throw new Error('that does not look like an API key (8-512 printable characters, no spaces)');
|
|
187
220
|
const p = typesafeKeyPath(env);
|
|
221
|
+
if ((opts.platform ?? process.platform) === 'win32') {
|
|
222
|
+
writeKeyWindows(p, k + '\n', opts.acl);
|
|
223
|
+
return p;
|
|
224
|
+
}
|
|
188
225
|
writeAtomic(p, k + '\n');
|
|
189
226
|
fs.chmodSync(p, 0o600);
|
|
190
227
|
return p;
|
|
191
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* Windows: the temp file is created EMPTY, locked to the current user and checked before the key goes into it, then
|
|
231
|
+
* renamed into place (a same-volume rename keeps its access list) and checked again. Any failure removes what was
|
|
232
|
+
* written and throws a message naming TYPESAFE_API_KEY.
|
|
233
|
+
*/
|
|
234
|
+
function writeKeyWindows(p, text, acl) {
|
|
235
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
236
|
+
const tmp = `${p}.${process.pid}.tmp`;
|
|
237
|
+
const fail = (what) => {
|
|
238
|
+
try {
|
|
239
|
+
fs.rmSync(tmp, { force: true });
|
|
240
|
+
}
|
|
241
|
+
catch { /* ignore */ }
|
|
242
|
+
throw new Error(`could not restrict the key file to your Windows account (${what}). Nothing was saved. Set TYPESAFE_API_KEY in your environment instead.`);
|
|
243
|
+
};
|
|
244
|
+
try {
|
|
245
|
+
fs.writeFileSync(tmp, '');
|
|
246
|
+
}
|
|
247
|
+
catch (e) {
|
|
248
|
+
fail(`create: ${e.code ?? 'error'}`);
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
lockOwnerOnly(tmp, currentUserSid(acl), acl);
|
|
252
|
+
}
|
|
253
|
+
catch (e) {
|
|
254
|
+
fail(`icacls: ${e instanceof Error ? e.message.split(/\r?\n/)[0] : String(e)}`);
|
|
255
|
+
}
|
|
256
|
+
const pre = checkOwnerOnly(tmp, acl);
|
|
257
|
+
if (!pre.ok)
|
|
258
|
+
fail(pre.reason);
|
|
259
|
+
fs.writeFileSync(tmp, text);
|
|
260
|
+
fs.renameSync(tmp, p);
|
|
261
|
+
const post = checkOwnerOnly(p, acl);
|
|
262
|
+
if (!post.ok) {
|
|
263
|
+
try {
|
|
264
|
+
fs.rmSync(p, { force: true });
|
|
265
|
+
}
|
|
266
|
+
catch { /* ignore */ }
|
|
267
|
+
fail(post.reason);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
192
270
|
export function removeTypesafeKey(env = process.env) {
|
|
193
271
|
try {
|
|
194
272
|
fs.rmSync(typesafeKeyPath(env));
|
|
@@ -8,6 +8,7 @@ export * from "./calibrate.js";
|
|
|
8
8
|
export * from "./classify.js";
|
|
9
9
|
export * from "./models.js";
|
|
10
10
|
export * from "./config.js";
|
|
11
|
+
export { checkOwnerOnly, currentUserSid, lockOwnerOnly, readSddl, sddlOwnerOnly } from "./win-acl.js";
|
|
11
12
|
export * from "./hardware.js";
|
|
12
13
|
export * from "./registry.js";
|
|
13
14
|
export { heuristicProvider, LARP_POINTS_PLATT, WIN_BASE_RATE } from "./providers/heuristic.js";
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// task-2147: owner-only secret files on Windows. NTFS has no POSIX mode bits (chmod only toggles read-only and
|
|
2
|
+
// stat reports 0666), so the mode cannot say who may read a file; its DACL can. Writes lock the file to the current
|
|
3
|
+
// user with icacls, by SID (account names are localized); reads check the DACL as SDDL (locale-free). Any probe
|
|
4
|
+
// failure refuses the file: the caller names the TYPESAFE_API_KEY env var as the way out.
|
|
5
|
+
import { execFileSync } from 'node:child_process';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
/**
|
|
8
|
+
* Well-known principals that may hold a key file besides its user (the set Windows OpenSSH accepts). `LA` is the
|
|
9
|
+
* built-in Administrator (RID 500), always a member of BA: SDDL writes that account's own SID in this alias form.
|
|
10
|
+
*/
|
|
11
|
+
const ALLOWED_WELL_KNOWN = new Set(['SY', 'S-1-5-18', 'BA', 'S-1-5-32-544', 'LA']);
|
|
12
|
+
/** The SDDL aliases that can stand for `sid` itself (task-2155: SDDL never spells RID 500 / 501 as S-1-…). */
|
|
13
|
+
const selfAliases = (sid) => (/-500$/.test(sid) ? ['LA'] : /-501$/.test(sid) ? ['LG'] : []);
|
|
14
|
+
const SID_RE = /^S-1-\d+(-\d+)+$/i;
|
|
15
|
+
const system32 = (...p) => path.win32.join(process.env.SystemRoot || process.env.windir || 'C:\\Windows', 'System32', ...p);
|
|
16
|
+
const defaultRun = (exe, args, env) => execFileSync(exe, args, { env, encoding: 'utf8', windowsHide: true, timeout: 15_000, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
17
|
+
/** The current user's SID (`whoami /user`, CSV: "host\user","S-1-5-21-..."). */
|
|
18
|
+
export function currentUserSid(deps = {}) {
|
|
19
|
+
const out = (deps.run ?? defaultRun)(system32('whoami.exe'), ['/user', '/fo', 'csv', '/nh'], process.env);
|
|
20
|
+
const sid = out.trim().split(/\r?\n/).pop()?.split(',').pop()?.replace(/"/g, '').trim() ?? '';
|
|
21
|
+
if (!SID_RE.test(sid))
|
|
22
|
+
throw new Error('could not read the current Windows account SID');
|
|
23
|
+
return sid;
|
|
24
|
+
}
|
|
25
|
+
/** Remove inherited access and grant only `sid` full control (icacls, no shell). */
|
|
26
|
+
export function lockOwnerOnly(p, sid, deps = {}) {
|
|
27
|
+
if (!SID_RE.test(sid))
|
|
28
|
+
throw new Error('bad SID');
|
|
29
|
+
(deps.run ?? defaultRun)(system32('icacls.exe'), [p, '/inheritance:r', '/grant:r', `*${sid}:(F)`], process.env);
|
|
30
|
+
}
|
|
31
|
+
/** The file's security descriptor as SDDL. The path goes through the environment, never into the command text. */
|
|
32
|
+
export function readSddl(p, deps = {}) {
|
|
33
|
+
const ps = system32('WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
34
|
+
return (deps.run ?? defaultRun)(ps, ['-NoProfile', '-NonInteractive', '-Command', '(Get-Acl -LiteralPath $env:DEXTERM_ACL_PATH).Sddl'], { ...process.env, DEXTERM_ACL_PATH: p }).replace(/\s+/g, ''); // SDDL has no whitespace: undo any console wrap
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Is this DACL owner-only? Every ACE that grants anything must name `sid` (as S-1-… or its alias), SYSTEM or
|
|
38
|
+
* Administrators. A missing or
|
|
39
|
+
* null DACL (everyone has access) and any ACE kind this parser does not know (e.g. conditional) are refused.
|
|
40
|
+
*/
|
|
41
|
+
export function sddlOwnerOnly(sddl, sid) {
|
|
42
|
+
const m = /D:([A-Z_]*)((?:\([^()]*\))*)(?:S:|$)/.exec(sddl.trim());
|
|
43
|
+
if (!m)
|
|
44
|
+
return { ok: false, reason: 'no readable access list' };
|
|
45
|
+
if (/NO_ACCESS_CONTROL/.test(m[1]))
|
|
46
|
+
return { ok: false, reason: 'no access list (everyone has access)' };
|
|
47
|
+
const others = new Set();
|
|
48
|
+
const self = new Set([sid.toUpperCase(), ...selfAliases(sid)]);
|
|
49
|
+
for (const ace of m[2].match(/\(([^()]*)\)/g) ?? []) {
|
|
50
|
+
const [type, flags = '', , , , who = ''] = ace.slice(1, -1).split(';');
|
|
51
|
+
if (type === 'D' || type === 'OD')
|
|
52
|
+
continue; // deny entries only take access away
|
|
53
|
+
if (type !== 'A' && type !== 'OA')
|
|
54
|
+
return { ok: false, reason: `unsupported access entry ${type}` };
|
|
55
|
+
if ((flags.match(/../g) ?? []).some((f) => f === 'IO'))
|
|
56
|
+
continue; // inherit-only (2-letter flags): not the file itself
|
|
57
|
+
const w = who.toUpperCase();
|
|
58
|
+
if (!self.has(w) && !ALLOWED_WELL_KNOWN.has(w))
|
|
59
|
+
others.add(w);
|
|
60
|
+
}
|
|
61
|
+
return others.size ? { ok: false, reason: `other accounts can access it (${[...others].join(', ')})` } : { ok: true };
|
|
62
|
+
}
|
|
63
|
+
/** Check a file's ACL: owner-only or a reason. Never throws. */
|
|
64
|
+
export function checkOwnerOnly(p, deps = {}) {
|
|
65
|
+
try {
|
|
66
|
+
return sddlOwnerOnly(readSddl(p, deps), currentUserSid(deps));
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
return { ok: false, reason: `its Windows access list could not be checked (${e instanceof Error ? e.message.split(/\r?\n/)[0] : String(e)})` };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -247,7 +247,7 @@ const TOPICS = [
|
|
|
247
247
|
body: '`dexterm autotrade run --all --paper` runs every strategy listed in ~/.dexterm/autotrade/autotrade.json at the same time on one data feed, to forward-test several ideas side by side. Each entry has an `id`, a `preset` (a shipped preset or your own strategy) or an absolute `file`, a `bankroll` in USD or SOL, optional `caps` that can only tighten it, and `enabled`. Each strategy gets its own paper book, run file, loss limits and breaker, sessions and model spend budget; a breaker or a stop on one never stops another. The screener fetches once for all of them, and the `global` limits (sessions open at once, model spend and sessions per day, one check of how busy the computer is) count across all of them. `dexterm autotrade status --all` and the runner view on the AUTOTRADE screen (`Q`) compare them: P/L, win rate with the closed count, rug exits and exposure, missed runners, spend per entry. `dexterm autotrade stop|flatten|pause|resume` act on every strategy; add `--strategy ID` to act on one, and `dexterm autotrade start ID` starts a listed one that is not running. An MCP agent can list and compare them and start or stop one listed strategy, never add one or change a size or a cap. Paper only: live is refused. Not financial advice. Full docs: https://dexterm.xyz/docs/autotrade/#several-strategies-at-once' },
|
|
248
248
|
{ id: 'autotrade-telegram', title: 'Your own Telegram bot for the autotrade runner: notify, control, one-tap approvals (dexterm telegram)', screen: 'all', tags: ['telegram', 'bot', 'phone', 'mobile', 'notify', 'notifications', 'approve', 'approval', 'approvals', 'pair', 'pairing', 'kill', 'pause', 'remote', 'botfather'],
|
|
249
249
|
commands: ['telegram'], feature: 'autotrade-telegram',
|
|
250
|
-
body: 'Create a bot with @BotFather, then run `dexterm telegram setup`: paste the token at the hidden prompt (never as an argument), dexterm checks it and keeps it in your keychain (or the system keyring, or a 0600 file), then shows a 6-digit code; send `/pair <code>` to your bot from your own private chat and confirm the account it shows (@username and id) with `y` on your computer. Pairing is refused while a webhook or another program polls the bot. Only that account in that chat is answered. `dexterm autotrade run <id> --paper` then starts the bot by itself (`--no-telegram` keeps it off): entries, exits, PnL cards, halts and a daily summary reach your phone; `/status`, `/positions`, `/pause`, `/resume` (a pause only), `/kill`, `/flatten` and `/use` (each after a confirm button, `/use` only to an equal-or-tighter strategy; a change it cannot rank counts as looser) and `/approvals on` work from chat, which can make things safer and never less safe. With `entry.decide: "agent+approve"` or approvals on, every entry is a card with [Approve] [Half size] [Skip]: only a button approves, once, for that proposal, your account and your private chat; the price and your limits are checked again at the tap; no answer is a skip. Typed messages are never trades. `dexterm telegram status|test|audit --verify|revoke`. Nothing is hosted; paper only; never send keys or seed phrases in chat. Not financial advice. Full docs: https://dexterm.xyz/docs/telegram/' },
|
|
250
|
+
body: 'Create a bot with @BotFather, then run `dexterm telegram setup`: paste the token at the hidden prompt (never as an argument), dexterm checks it and keeps it in your keychain (or the system keyring, or a 0600 file), then shows a 6-digit code; send `/pair <code>` to your bot from your own private chat and confirm the account it shows (@username and id) with `y` on your computer. Pairing is refused while a webhook or another program polls the bot. Only that account in that chat is answered. `dexterm autotrade run <id> --paper` then starts the bot by itself (`--no-telegram` keeps it off): entries, exits, PnL cards, halts and a daily summary reach your phone; `/status`, `/positions`, `/pause`, `/resume` (a pause only), `/kill`, `/flatten` and `/use` (each after a confirm button, `/use` only to an equal-or-tighter strategy; a change it cannot rank counts as looser) and `/approvals on` work from chat, which can make things safer and never less safe. Read-only from chat: `/check <CA>` (one coin\'s LARP score and risk facts: authorities, top-10 holders, dev share, LP lock, liquidity, each with the age of its data), `/top [N]` (the screener\'s trending list, 1 to 10, the runner\'s own coins marked) and `/signals [N]` (the last 1 to 20 signals the runner raised: screen matches, safety refusals, the drain guard, stuck bags, halts, fills); data, not advice, and they cannot reach any write. With `entry.decide: "agent+approve"` or approvals on, every entry is a card with [Approve] [Half size] [Skip]: only a button approves, once, for that proposal, your account and your private chat; the price and your limits are checked again at the tap; no answer is a skip. Typed messages are never trades. `dexterm telegram status|test|audit --verify|revoke`. Nothing is hosted; paper only; never send keys or seed phrases in chat. Not financial advice. Full docs: https://dexterm.xyz/docs/telegram/' },
|
|
251
251
|
{ id: 'autotrade-real-signal', title: 'Real mode: send a strategy\'s trades to your own executor as LIVE TRADE FIRE signals (dexterm autotrade signal)', screen: 'all', tags: ['real', 'mode', 'live', 'trade', 'fire', 'signal', 'signals', 'executor', 'webhook', 'hook', 'jsonl', 'receipt', 'receipts', 'arm', 'caps', 'kill', 'outbox', 'nonce'],
|
|
252
252
|
commands: ['autotrade'], tools: ['trade_fire_feed'], feature: 'autotrade-real-signal',
|
|
253
253
|
body: 'Real mode runs a strategy exactly like paper (the same clamp, exits, monitor and run file) and sends every buy and sell it fills, first, as a LIVE TRADE FIRE signal (`dexterm-trade-fire/1`: buy or sell, mint, symbol, size in SOL and % of book, the sell fraction and exit reason, strategy, run, account, an always-growing nonce, an idempotency id and an expiry) to YOUR executor: a Telegram trading bot of your choice or any script. dexterm holds no key and signs nothing. Set channels with `dexterm autotrade signal channels --webhook URL | --signal-cmd \'["/abs/path"]\' | --jsonl on | --mcp on | --telegram on` (they live in connectors.json next to the Telegram setup; a bot cannot read another bot, so the Telegram copy alone is not enough), a webhook HMAC secret with `signal secret generate --out FILE`, hard caps with `signal caps set` (raising needs a typed confirmation), then arm on your own terminal with `dexterm autotrade signal arm <id>` (evidence panel + a typed phrase with a one-time code; any later edit disarms; below the evidence floor, a completed forward test with your `minForwardTrades` closed paper trades and a backtest of these exact rules, or past a missing rung of the ladder, it refuses unless you first type a separate OVERRIDE phrase, which is journaled) and run `dexterm autotrade run <id> --real` (live data only, in the foreground: in 0.2.0 the always-on service runs paper strategies only). Delivery is at least once from a restart-safe outbox, in nonce order, retried until acked or expired; the executor dedupes by nonce, ignores expired signals and acts only on its own account. Every run ends with a `halted` signal that lists the positions still open; the run\'s last lines and the Telegram copy name them too. Open positions are carried to the next real run of the same strategy and account, whose exits keep firing; while no real run is running NO signal goes out, sells included, so manage them in your executor. Positions stay unconfirmed until `dexterm autotrade receipt <nonce> executed|rejected|failed` (data only). `signal kill` stops every signal; `signal status`, `signal tail`, `receipt list` and `autotrade audit` show the outbox, receipts and drift; agents can read the MCP feed with trade_fire_feed. No model, tool or chat tap can arm real mode, raise a cap or lift the kill switch. Experimental; not financial advice.' },
|
|
@@ -25,7 +25,7 @@ import { appendFileSync } from 'node:fs';
|
|
|
25
25
|
import { join } from 'node:path';
|
|
26
26
|
import { dextermHome } from '../../../custom/src/index.js';
|
|
27
27
|
import { KillSwitch, createDataLayer, defaultDbPath, resolveRpcUrl } from '../../../data/src/index.js';
|
|
28
|
-
import { DrainPoller, RugProbe, chainVets, defaultFetchJson, rugproofVet } from '../../../autotrade/src/index.js';
|
|
28
|
+
import { DrainPoller, RugProbe, chainVets, defaultFetchJson, readOnlyCall, rugproofVet } from '../../../autotrade/src/index.js';
|
|
29
29
|
import { FleetGuard, HEARTBEAT_MS, accountTable, LiveSource, RUNNER_RENDEZVOUS_SCHEMA, RUN_SUMMARY_SCHEMA, RunnerSafety, acquireRunLock, bankrollUsd, claudeSessionFactory, clearControl, clearMemberControls, clearStaleControl, compareRows, compareTable, controlPath, fileLedger, fleetStatusPath, fleetTotals, loadFleetConfig, loadRunnerConfig, loadTape, memberStrategy, newRunId, prepareAccount, releaseRunLock, renderDossierRecord, runFleet, runFiles, runnerRendezvousPath, runnerSystemPrompt, sharedDossier, stubSession, writeJsonAtomic, writeRendezvous, } from '../../../autotrade/src/index.js';
|
|
30
30
|
import { parseArgs, resolveStrategyArg } from "./strategies.js";
|
|
31
31
|
import { larpVet } from "./run.js";
|
|
@@ -122,7 +122,8 @@ export async function fleetRunCmd(argv, io, over = {}) {
|
|
|
122
122
|
const rpc = resolveRpcUrl({ env: io.env });
|
|
123
123
|
data = createDataLayer({ dbPath: io.env.DEXTERM_DB_PATH || defaultDbPath(io.env), rpcUrl: rpc.url, rpcFallbackUrl: rpc.fallbackUrl });
|
|
124
124
|
}
|
|
125
|
-
const closeData = () => {
|
|
125
|
+
const closeData = () => { if (data)
|
|
126
|
+
over.onReadTools?.(null); try {
|
|
126
127
|
data?.close();
|
|
127
128
|
}
|
|
128
129
|
catch { /* closing */ } };
|
|
@@ -227,6 +228,7 @@ export async function fleetRunCmd(argv, io, over = {}) {
|
|
|
227
228
|
// One dossier record per coin per 30 s, whichever strategy asks; each strategy renders its own text.
|
|
228
229
|
const { McpTools } = await import('../../../mcp/src/index.js');
|
|
229
230
|
const mcp = new McpTools(data, () => ({ screen: 'autotrade-runner', actions: { mints: [], feeds: [], sortKeys: [] } }));
|
|
231
|
+
over.onReadTools?.(readOnlyCall(mcp));
|
|
230
232
|
const shared = sharedDossier(async (mint) => {
|
|
231
233
|
try {
|
|
232
234
|
return await mcp.call('get_dossier', { mint, interval: '5m' });
|
|
@@ -29,7 +29,7 @@ import { join } from 'node:path';
|
|
|
29
29
|
import { dextermHome, loadConfig } from '../../../custom/src/index.js';
|
|
30
30
|
import { pickBasis } from '../../../data/src/basis.js';
|
|
31
31
|
import { KillSwitch, createDataLayer, defaultDbPath, resolveRpcUrl } from '../../../data/src/index.js';
|
|
32
|
-
import { DrainPoller, RugProbe, chainVets, defaultFetchJson, rugproofVet } from '../../../autotrade/src/index.js';
|
|
32
|
+
import { DrainPoller, RugProbe, chainVets, defaultFetchJson, readOnlyCall, rugproofVet } from '../../../autotrade/src/index.js';
|
|
33
33
|
import { HEARTBEAT_MS, LiveSource, badgeOf, RUNNER_RENDEZVOUS_SCHEMA, RunnerSafety, SessionGate, acquireRunLock, clearControl, clearStaleControl, controlPath, claudeSessionFactory, effectiveCaps, fileLedger, loadRunnerConfig, loadTape, newRunId, prepareAccount, releaseRunLock, renderDossierRecord, runAutotrade, runnerRendezvousPath, runnerSystemPrompt, simExecutor, stubSession, writeJsonAtomic, writeRendezvous, RUN_SUMMARY_SCHEMA, builtinTapePath, } from '../../../autotrade/src/index.js';
|
|
34
34
|
import { assertAutotradeAllowed, ollamaSessionFactory } from '../../../autotrade/src/ollama-sessions.js';
|
|
35
35
|
import { Outbox, RealSignalExecutor, channelsFromConfig, enabledChannels, loadSignalsConfig, openRealRun, realModeCheck } from '../../../autotrade/src/index.js';
|
|
@@ -313,13 +313,15 @@ export async function runCmd(argv, io, over = {}) {
|
|
|
313
313
|
else {
|
|
314
314
|
const rpc = resolveRpcUrl({ env: io.env });
|
|
315
315
|
const data = createDataLayer({ dbPath: io.env.DEXTERM_DB_PATH || defaultDbPath(io.env), rpcUrl: rpc.url, rpcFallbackUrl: rpc.fallbackUrl });
|
|
316
|
-
closeData = () => data.close();
|
|
316
|
+
closeData = () => { host?.setReadTools(null); data.close(); };
|
|
317
317
|
liveData = data.dexscreener;
|
|
318
318
|
live = new LiveSource({ store: data.store, dexscreener: data.dexscreener }, { onLog: (l) => log('source_error', { error: l.slice(0, 300) }) });
|
|
319
319
|
source = { name: 'live keyless data layer (Store + DexScreener tokens/v1 fast lane)', ticks: live.ticks(), startT: startedAt };
|
|
320
320
|
// v1 dossier: the MCP get_dossier assembly (market, holders, security, candles, tape, creator) for the mint.
|
|
321
321
|
const { McpTools } = await import('../../../mcp/src/index.js');
|
|
322
322
|
const mcp = new McpTools(data, () => ({ screen: 'autotrade-runner', actions: { mints: [], feeds: [], sortKeys: [] } }));
|
|
323
|
+
// task-1426: the bot's /check and /top read the same tools (read tools only), while this run's data layer is open.
|
|
324
|
+
host?.setReadTools(readOnlyCall(mcp));
|
|
323
325
|
dossier = async (k, h, matched, b) => {
|
|
324
326
|
let rec;
|
|
325
327
|
try {
|
|
@@ -470,6 +470,8 @@ async function runServiceCmd(argv, io, deps) {
|
|
|
470
470
|
const over = {
|
|
471
471
|
...deps.fleetOver,
|
|
472
472
|
extraSafety: () => ({ pauseEntries: lastGuard?.pauseEntries ?? null }),
|
|
473
|
+
// task-1426: the fleet's read tools for the bot's /check and /top (the service opens no data layer of its own).
|
|
474
|
+
onReadTools: (call) => { tg?.setReadTools(call); deps.fleetOver?.onReadTools?.(call); },
|
|
473
475
|
vetWrap: takenVet,
|
|
474
476
|
adopt: (id) => adopt.get(id),
|
|
475
477
|
onStarted: (id, fl) => {
|
|
@@ -587,7 +589,7 @@ async function startTelegram(home, runId, say) {
|
|
|
587
589
|
} };
|
|
588
590
|
const svc = new TelegramService({ home, connector, port, knownToken: () => token, runId, state, strategy: () => null, alert: (l) => say(`! ${l}`) });
|
|
589
591
|
await svc.start();
|
|
590
|
-
say('telegram: your bot is on (notifications, /status, /pause, /stop)');
|
|
592
|
+
say('telegram: your bot is on (notifications, /status, /pause, /stop, /check, /top, /signals)');
|
|
591
593
|
return svc;
|
|
592
594
|
}
|
|
593
595
|
// ───────────────────────────── status / logs ─────────────────────────────
|
|
@@ -73,7 +73,7 @@ export function listCmd(argv, io) {
|
|
|
73
73
|
io.stdout.write(`${head} INVALID: ${(r.errors ?? []).slice(0, 2).map(fmtIssue).join('; ')}\n`);
|
|
74
74
|
continue;
|
|
75
75
|
}
|
|
76
|
-
io.stdout.write(`${head} v${r.version.padEnd(7)} ${r.hash} ${r.mode.padEnd(5)} profiles ${r.profiles.join(',')} · ${bt}${r.highRisk ? ' · ⚠ HIGH RISK' : ''}\n`);
|
|
76
|
+
io.stdout.write(`${head} v${r.version.padEnd(7)} ${r.hash} ${r.mode.padEnd(5)} profiles ${r.profiles.join(',')} · ${bt}${r.highRisk ? ' · ⚠ HIGH RISK' : ''}${r.experimentalTiers ? ' · EXPERIMENTAL tiers (unproven, paper only)' : ''}\n`);
|
|
77
77
|
}
|
|
78
78
|
if (rows.some((r) => r.shadowsPreset))
|
|
79
79
|
io.stdout.write(' * your file shadows the shipped preset of the same id\n');
|
|
@@ -47,6 +47,8 @@ export class TelegramHost {
|
|
|
47
47
|
if (solUsdFor)
|
|
48
48
|
this.solUsdFor = solUsdFor;
|
|
49
49
|
}
|
|
50
|
+
/** task-1426: the run's read tools for /check and /top (null when its data layer closes, or on a replay). */
|
|
51
|
+
setReadTools(call) { this.service.setReadTools(call); }
|
|
50
52
|
onLedger = (e) => this.service.onLedger(e);
|
|
51
53
|
onState = (st) => { this.state = st; this.service.onState(st); };
|
|
52
54
|
/** The strategy a confirmed /use asked for, once (null when none). */
|
|
@@ -5,7 +5,7 @@ import { spawn } from 'node:child_process';
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import readline from 'node:readline';
|
|
8
|
-
import { CATALOG, COIN_RISK, EXTERNAL_MODELS, PROVIDERS, PROVIDER_IDS, catalogEntry, classify, classifyBatch, coinDossier, describeHardware, describeResult, detectHardware, dextermHome, fits, freeBytes, fromReasonRunner, gb, isInstalled, loadClassifierConfig, modelPath, modelsDir, providerFromConfig, pullModel, readTypesafeKey, recommendTier, removeTypesafeKey, saveClassifierConfig, serveArgv, writeTypesafeKey, } from '../../classifier/src/index.js';
|
|
8
|
+
import { CATALOG, COIN_RISK, EXTERNAL_MODELS, PROVIDERS, PROVIDER_IDS, catalogEntry, classify, classifyBatch, coinDossier, describeHardware, describeResult, detectHardware, dextermHome, fits, freeBytes, fromReasonRunner, gb, isInstalled, loadClassifierConfig, modelPath, modelsDir, providerFromConfig, pullModel, readTypesafeKey, recommendTier, removeTypesafeKey, saveClassifierConfig, serveArgv, typesafeKeyRefusal, typesafeKeyState, writeTypesafeKey, } from '../../classifier/src/index.js';
|
|
9
9
|
import { spawnClaudeRunner } from '../../packs/src/index.js';
|
|
10
10
|
export const CLASSIFIER_USAGE = `dexterm classifier [<command>]
|
|
11
11
|
list providers, local models (installed / size / licence / fits this machine) and the current pick
|
|
@@ -15,7 +15,7 @@ export const CLASSIFIER_USAGE = `dexterm classifier [<command>]
|
|
|
15
15
|
doctor [--json] hardware (RAM, CPU, GPU/VRAM), the recommended tier, provider readiness
|
|
16
16
|
bench [--n 20] time the current provider on N synthetic coins (latency per coin, per 1,000)
|
|
17
17
|
try classify one example coin with the current provider (shows the labelled estimate)
|
|
18
|
-
key set|clear|status your own TypeSafe API key for hosted Jev (stored
|
|
18
|
+
key set|clear|status your own TypeSafe API key for hosted Jev (stored owner-only under ~/.dexterm/keys, never printed)
|
|
19
19
|
Every answer is a model estimate with the model id and a calibration note; never advice. Docs: /docs/classifier/`;
|
|
20
20
|
function flags(argv, bools) {
|
|
21
21
|
const pos = [];
|
|
@@ -34,6 +34,7 @@ function flags(argv, bools) {
|
|
|
34
34
|
}
|
|
35
35
|
return { pos, opts };
|
|
36
36
|
}
|
|
37
|
+
const keyLine = (s) => s.source === 'refused' ? 'refused (dexterm classifier key status says why; TYPESAFE_API_KEY works instead)' : s.source === 'none' ? 'none (hosted Jev is optional)' : 'configured';
|
|
37
38
|
async function ask(io, question) {
|
|
38
39
|
if (!io.stdin.isTTY)
|
|
39
40
|
return '';
|
|
@@ -178,7 +179,7 @@ export async function classifierCommand(argv, io) {
|
|
|
178
179
|
if (!isInstalled(home, catalogEntry(model)))
|
|
179
180
|
out(`note: ${model} is not downloaded yet: dexterm classifier pull ${model}. Until it is served, answers fall back to the heuristic.`);
|
|
180
181
|
}
|
|
181
|
-
if (id === 'jev-hosted' && !readTypesafeKey(io.env))
|
|
182
|
+
if (id === 'jev-hosted' && !readTypesafeKey(io.env, { platform: io.platform, acl: io.acl }))
|
|
182
183
|
out('note: no TypeSafe key yet: dexterm classifier key set. Until then answers fall back to the heuristic.');
|
|
183
184
|
const p = saveClassifierConfig(next, io.env);
|
|
184
185
|
out(`classifier.provider = ${id}${id === 'kev-local' ? ` (${next.kev.model})` : ''} (${p})`);
|
|
@@ -274,13 +275,13 @@ export async function classifierCommand(argv, io) {
|
|
|
274
275
|
const llama = llamaServerBin(io.env);
|
|
275
276
|
const llamaOk = llama ? await llamaSupportsDecisions(llama, io.exec) : false;
|
|
276
277
|
if (opts.json === true) {
|
|
277
|
-
out(JSON.stringify({ hardware: hw, recommendation: rec, provider: c.provider, model: c.provider === 'kev-local' ? c.kev.model : null, ready: provStatus.ready, detail: provStatus.detail, llamaServer: Boolean(llama), llamaServerDecisions: llamaOk, typesafeKey: Boolean(readTypesafeKey(io.env)) }, null, 2));
|
|
278
|
+
out(JSON.stringify({ hardware: hw, recommendation: rec, provider: c.provider, model: c.provider === 'kev-local' ? c.kev.model : null, ready: provStatus.ready, detail: provStatus.detail, llamaServer: Boolean(llama), llamaServerDecisions: llamaOk, typesafeKey: Boolean(readTypesafeKey(io.env, { platform: io.platform, acl: io.acl })) }, null, 2));
|
|
278
279
|
return 0;
|
|
279
280
|
}
|
|
280
281
|
out(`hardware: ${describeHardware(hw)}`);
|
|
281
282
|
out(`recommended: ${rec.model ?? 'the built-in heuristic'}${rec.bestQuality ? ` (best quality here: ${rec.bestQuality})` : ''}: ${rec.why}`);
|
|
282
283
|
out(`provider: ${c.provider}${c.provider === 'kev-local' ? ` (${c.kev.model})` : ''}: ${provStatus.ready ? 'ready' : 'not ready'}: ${provStatus.detail}`);
|
|
283
|
-
out(`llama-server: ${!llama ? 'not found (needed for kev-local)' : llamaOk ? 'found, serves decision models' : `found but too old: ${LLAMA_TOO_OLD}`} · TypeSafe key: ${
|
|
284
|
+
out(`llama-server: ${!llama ? 'not found (needed for kev-local)' : llamaOk ? 'found, serves decision models' : `found but too old: ${LLAMA_TOO_OLD}`} · TypeSafe key: ${keyLine(typesafeKeyState(io.env, { platform: io.platform, acl: io.acl, fresh: true }))}`);
|
|
284
285
|
if (c.fallback)
|
|
285
286
|
out('fallback: the heuristic answers when the provider is unreachable (labelled as a fallback)');
|
|
286
287
|
return 0;
|
|
@@ -307,8 +308,15 @@ export async function classifierCommand(argv, io) {
|
|
|
307
308
|
}
|
|
308
309
|
if (sub === 'key') {
|
|
309
310
|
const action = pos[0] ?? 'status';
|
|
311
|
+
const keyOpts = { platform: io.platform, acl: io.acl };
|
|
310
312
|
if (action === 'status') {
|
|
311
|
-
|
|
313
|
+
const s = typesafeKeyState(io.env, { ...keyOpts, fresh: true });
|
|
314
|
+
if (s.source === 'refused') {
|
|
315
|
+
out('TypeSafe key: refused');
|
|
316
|
+
err(typesafeKeyRefusal(s));
|
|
317
|
+
return 1;
|
|
318
|
+
}
|
|
319
|
+
out(s.source === 'env' ? 'TypeSafe key: configured from TYPESAFE_API_KEY (not shown)' : s.source === 'file' ? 'TypeSafe key: configured (not shown)' : 'TypeSafe key: none');
|
|
312
320
|
return 0;
|
|
313
321
|
}
|
|
314
322
|
if (action === 'clear') {
|
|
@@ -322,8 +330,8 @@ export async function classifierCommand(argv, io) {
|
|
|
322
330
|
return 1;
|
|
323
331
|
}
|
|
324
332
|
try {
|
|
325
|
-
const p = writeTypesafeKey(k, io.env);
|
|
326
|
-
out(`saved to ${p} (0600). It is only sent to ${c.jev.endpoint}.`);
|
|
333
|
+
const p = writeTypesafeKey(k, io.env, keyOpts);
|
|
334
|
+
out(`saved to ${p} (${(io.platform ?? process.platform) === 'win32' ? 'owner-only Windows access list' : '0600'}). It is only sent to ${c.jev.endpoint}.`);
|
|
327
335
|
return 0;
|
|
328
336
|
}
|
|
329
337
|
catch (x) {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// symbol is written (the name and symbol are read only to compute the domain-match boolean).
|
|
12
12
|
import { linkFeatures } from '../../data/src/index.js';
|
|
13
13
|
import { LABELER_VERSION } from '../../recorder/src/index.js';
|
|
14
|
-
import { SKILL_EVAL_SCHEMA } from '../../packs/src/index.js';
|
|
14
|
+
import { SKILL_EVAL_SCHEMA, cutoffIso } from '../../packs/src/index.js';
|
|
15
15
|
function readCache(db, ns, mint) {
|
|
16
16
|
const row = db.prepare('SELECT json, fetched_at FROM cache WHERE ns = ? AND key = ?').get(ns, mint);
|
|
17
17
|
if (!row)
|
|
@@ -47,6 +47,11 @@ export function buildSkillEvalSet(db, opts = {}) {
|
|
|
47
47
|
let picked = null;
|
|
48
48
|
for (const o of outs) {
|
|
49
49
|
const t0 = Number(o.t0);
|
|
50
|
+
// A t0 that is not a valid time can never anchor point-in-time evidence (and would poison the split). task-1982
|
|
51
|
+
if (Number.isNaN(new Date(t0).getTime())) {
|
|
52
|
+
excluded.bad_t0 = (excluded.bad_t0 ?? 0) + 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
50
55
|
const srcs = [['jupiter', jup], ['dexscreener', ds]]
|
|
51
56
|
.filter((x) => !!x[1] && x[1].fetched_at >= t0 && x[1].fetched_at <= t0 + lagMs);
|
|
52
57
|
if (srcs.length) {
|
|
@@ -101,7 +106,7 @@ export function buildSkillEvalSet(db, opts = {}) {
|
|
|
101
106
|
horizon_sec: horizon,
|
|
102
107
|
label_rule: `recorder outcome at ${horizon}s (keyless proxy, labeler ${LABELER_VERSION}); bad = rug ∪ dead, ok = flat ∪ fade ∪ runner; unobserved dropped`,
|
|
103
108
|
evidence_rule: `link features from the cached DexScreener/Jupiter token record only when it was fetched within ${lagMs / 1000}s after the row's t0 (latest such t0 per coin); rug-check features from that t0 snapshot`,
|
|
104
|
-
split_rule: `by time: the latest ${Math.round(testFrac * 100)}% of rows by t0 (t0 ≥ ${
|
|
109
|
+
split_rule: `by time: the latest ${Math.round(testFrac * 100)}% of rows by t0 (t0 ≥ ${cutoffIso(cutoff)}) are the held-out TEST split, fixed before tuning`,
|
|
105
110
|
cutoff_t0: cutoff,
|
|
106
111
|
counts: { rows: rows.length, train: train.length, test: test.length, bad_train: train.filter((r) => r.bad).length, bad_test: test.filter((r) => r.bad).length, excluded },
|
|
107
112
|
rows,
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// The one place the app's version lives (task-1397). --version, the help banner and `dexterm update` read it;
|
|
2
2
|
// cli.test.ts keeps it equal to packages/cli/package.json (release.yml refuses a tag that differs).
|
|
3
|
-
export const VERSION = '0.2.
|
|
3
|
+
export const VERSION = '0.2.2';
|
|
@@ -35,6 +35,6 @@ sources:
|
|
|
35
35
|
date: 2026-10-16
|
|
36
36
|
---
|
|
37
37
|
|
|
38
|
-
Create a bot with @BotFather, then run `dexterm telegram setup`: paste the token at the hidden prompt (never as an argument), dexterm checks it and keeps it in your keychain (or the system keyring, or a 0600 file), then shows a 6-digit code; send `/pair <code>` to your bot from your own private chat and confirm the account it shows (@username and id) with `y` on your computer. Pairing is refused while a webhook or another program polls the bot. Only that account in that chat is answered. `dexterm autotrade run <id> --paper` then starts the bot by itself (`--no-telegram` keeps it off): entries, exits, PnL cards, halts and a daily summary reach your phone; `/status`, `/positions`, `/pause`, `/resume` (a pause only), `/kill`, `/flatten` and `/use` (each after a confirm button, `/use` only to an equal-or-tighter strategy; a change it cannot rank counts as looser) and `/approvals on` work from chat, which can make things safer and never less safe. With `entry.decide: "agent+approve"` or approvals on, every entry is a card with [Approve] [Half size] [Skip]: only a button approves, once, for that proposal, your account and your private chat; the price and your limits are checked again at the tap; no answer is a skip. Typed messages are never trades. `dexterm telegram status|test|audit --verify|revoke`. Nothing is hosted; paper only; never send keys or seed phrases in chat. Not financial advice. Full docs: https://dexterm.xyz/docs/telegram/
|
|
38
|
+
Create a bot with @BotFather, then run `dexterm telegram setup`: paste the token at the hidden prompt (never as an argument), dexterm checks it and keeps it in your keychain (or the system keyring, or a 0600 file), then shows a 6-digit code; send `/pair <code>` to your bot from your own private chat and confirm the account it shows (@username and id) with `y` on your computer. Pairing is refused while a webhook or another program polls the bot. Only that account in that chat is answered. `dexterm autotrade run <id> --paper` then starts the bot by itself (`--no-telegram` keeps it off): entries, exits, PnL cards, halts and a daily summary reach your phone; `/status`, `/positions`, `/pause`, `/resume` (a pause only), `/kill`, `/flatten` and `/use` (each after a confirm button, `/use` only to an equal-or-tighter strategy; a change it cannot rank counts as looser) and `/approvals on` work from chat, which can make things safer and never less safe. Read-only from chat: `/check <CA>` (one coin's LARP score and risk facts: authorities, top-10 holders, dev share, LP lock, liquidity, each with the age of its data), `/top [N]` (the screener's trending list, 1 to 10, the runner's own coins marked) and `/signals [N]` (the last 1 to 20 signals the runner raised: screen matches, safety refusals, the drain guard, stuck bags, halts, fills); data, not advice, and they cannot reach any write. With `entry.decide: "agent+approve"` or approvals on, every entry is a card with [Approve] [Half size] [Skip]: only a button approves, once, for that proposal, your account and your private chat; the price and your limits are checked again at the tap; no answer is a skip. Typed messages are never trades. `dexterm telegram status|test|audit --verify|revoke`. Nothing is hosted; paper only; never send keys or seed phrases in chat. Not financial advice. Full docs: https://dexterm.xyz/docs/telegram/
|
|
39
39
|
|
|
40
40
|
CLI: `dexterm telegram`.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: docs-autotrade-confidence-tiers-experimental-unproven-paper-only
|
|
3
|
+
title: Autotrade (paper) › Confidence tiers (experimental, unproven, paper only)
|
|
4
|
+
kind: docs
|
|
5
|
+
page: autotrade
|
|
6
|
+
page_title: Autotrade (paper)
|
|
7
|
+
heading: Confidence tiers (experimental, unproven, paper only)
|
|
8
|
+
anchor: confidence-tiers-experimental-unproven-paper-only
|
|
9
|
+
url: https://dexterm.xyz/docs/autotrade#confidence-tiers-experimental-unproven-paper-only
|
|
10
|
+
tags:
|
|
11
|
+
- docs
|
|
12
|
+
- autotrade
|
|
13
|
+
- confidence
|
|
14
|
+
- tiers
|
|
15
|
+
- experimental
|
|
16
|
+
- unproven
|
|
17
|
+
- paper
|
|
18
|
+
- only
|
|
19
|
+
confidence: high
|
|
20
|
+
owner: maintainers
|
|
21
|
+
origin: docs-generated
|
|
22
|
+
reviewed_by: generated@2026-10-16
|
|
23
|
+
generated_from: dexterm/site content/docs (task-1420)
|
|
24
|
+
sources:
|
|
25
|
+
- site/content/docs/autotrade.md
|
|
26
|
+
date: 2026-10-16
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
**Unproven.** On our held-out data no tier made money, and our November research found no dexterm strategy with a proven edge. This is a way to test a sizing rule on paper; it is not a signal and not a reason to trade real money. Off unless a strategy turns it on, and refused outside paper.
|
|
30
|
+
|
|
31
|
+
Turn it on per strategy with `sizing.tiers` (paper mode, and the rug-proof block (https://dexterm.xyz/docs/autotrade#rug-proof-entries-experimental) with `minPoolAgeMin` 10 or more):
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
"sizing": { "tiers": { "table": "v4-1785", "experimental": "paper-only" } }
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The engine, not the agent, then scores every candidate on two figures from that coin's own tick:
|
|
38
|
+
|
|
39
|
+
- **turnover1h** = 1 h volume / liquidity, a point at **9.878 or less**;
|
|
40
|
+
- **bs5m** = buys / sells over 5 minutes, a point at **3.714 or more**.
|
|
41
|
+
|
|
42
|
+
Score 0 is **T0, a skip**: no entry. Score 1 is T3, score 2 is T4. **No tier runs above 25% of the book**: the table configures T3 at 75% and T4 at 100%, and both run at 25%. The cap is in the engine's code; no strategy field, table entry or agent answer lifts it, and it also holds across the scale-in tranches of a tiered position. After that every limit you set still applies and the stricter one wins (`maxPctOfBankroll`, `maxUsd`, the profile's max, per-trade loss, exposure, cash, price impact, the drawdown size-down, the rug-proof young-pool cap). The agent can ask for less, never more, and its confidence does not change the tier. One open position at a time unless you add `"oneOpen": false`; an entry still waiting for its fill (the execution latency) or for your approval counts as open, for this and for `maxConcurrent`, and its dollars count against your exposure and cash.
|
|
43
|
+
|
|
44
|
+
A tier also needs, or it is T0: liquidity of $30,000 or more, a token at least 10 minutes old, not young (under 2 h) and vertical (1 h change of 150% or more), a 1 h change at all for a young coin, and a live tick no older than 5 minutes at the moment of the decision. A missing, negative or non-numeric figure never scores a point.
|
|
45
|
+
|
|
46
|
+
Paper only, three times over: `dexterm autotrade validate` refuses the block in mode `real` or `live`, the runner refuses it in a real-signal run, and the engine refuses it outside paper.
|
|
47
|
+
|
|
48
|
+
Where you see it: `dexterm autotrade validate <id>` prints the verdict and every tier with its held-out numbers, `dexterm autotrade list` marks the strategy `EXPERIMENTAL tiers (unproven, paper only)`, every candidate writes a `TIER` line in the run log with its score, size and the verdict, and the ledger's clamp event records the tier.
|
|
49
|
+
|
|
50
|
+
The table's held-out numbers (frozen before the held-out set was scored once, per entry, 95% interval):
|
|
51
|
+
|
|
52
|
+
| tier | when | runs at | held-out |
|
|
53
|
+
|---|---|---|---|
|
|
54
|
+
| T0 | score 0 or a failed gate | skip | 19 entries, mean -17.0% (-35.7% … +3.4%) |
|
|
55
|
+
| T3 | score 1 | 25% | 17 entries, mean -7.6% (-31.8% … +16.8%) |
|
|
56
|
+
| T4 | score 2 | 25% | no entries (too few to say anything) |
|
|
@@ -27,8 +27,10 @@ date: 2026-10-16
|
|
|
27
27
|
---
|
|
28
28
|
|
|
29
29
|
```
|
|
30
|
-
dexterm classifier key set paste your TypeSafe API key (hidden input; stored
|
|
30
|
+
dexterm classifier key set paste your TypeSafe API key (hidden input; stored owner-only under ~/.dexterm/keys)
|
|
31
31
|
dexterm classifier pick jev-hosted
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
dexterm never ships or shares a key. The key is sent only to `https://api.typesafe.ai`, never printed, and never stored in `classifier.json`. `TYPESAFE_API_KEY` in the environment works too. Each call counts tokens. Set your price per million input tokens in `~/.dexterm/classifier.json` (`jev.pricePerMTokUsd`) to see a cost in dollars; until you do, the cost shows as *unpriced*, never as $0. A daily call cap (`jev.maxCallsPerDay`, default 1,000) stops calls before they reach the network. A rate limit from the API is reported with its retry time.
|
|
35
|
+
|
|
36
|
+
The key file must be readable by your account only: on macOS and Linux it is saved with mode 0600, and a file other users can read is refused. On Windows (not supported, see Windows expectations (https://dexterm.xyz/docs/beginners#windows-expectations)) file modes mean nothing, so `key set` locks the file to your Windows account with `icacls` and every read checks its access list; a file other accounts can read, or one whose access list cannot be checked, is refused. `dexterm classifier key status` says why a file is refused. If `icacls` or PowerShell is blocked on your machine, set `TYPESAFE_API_KEY` in your environment instead.
|
|
@@ -41,10 +41,10 @@ $ dexterm doctor
|
|
|
41
41
|
The package is named `dexterm-cli` because npm refuses the name `dexterm` as too similar to `xterm`. The command it installs is `dexterm`. The package ships compiled JavaScript, so it needs no build step. Every GitHub release carries a build-provenance attestation and `SHA256SUMS` you can verify (see the trust page (https://dexterm.xyz/trust/)).
|
|
42
42
|
|
|
43
43
|
```sh install from the release
|
|
44
|
-
$ # download dexterm-0.2.
|
|
44
|
+
$ # download dexterm-0.2.2-src.tar.gz and SHA256SUMS from the releases page, then:
|
|
45
45
|
$ shasum -a 256 -c SHA256SUMS --ignore-missing
|
|
46
|
-
$ gh attestation verify dexterm-0.2.
|
|
47
|
-
$ tar -xzf dexterm-0.2.
|
|
46
|
+
$ gh attestation verify dexterm-0.2.2-src.tar.gz --repo dexterm-xyz/dexterm
|
|
47
|
+
$ tar -xzf dexterm-0.2.2-src.tar.gz && cd dexterm-0.2.2
|
|
48
48
|
$ ./install.sh
|
|
49
49
|
$ dexterm doctor
|
|
50
50
|
```
|
|
@@ -32,4 +32,4 @@ date: 2026-10-16
|
|
|
32
32
|
| optional: a free RPC key | the ranked top-20 holder list and faster chain reads | Helius, Alchemy, QuickNode or Ankr free tier; see configuration (https://dexterm.xyz/docs/configuration). |
|
|
33
33
|
| optional: Rust (`cargo`) | the native docked chart window | without it the chart popup opens in your browser; see popup chart (https://dexterm.xyz/docs/popup-chart). |
|
|
34
34
|
|
|
35
|
-
> note: dexterm 0.2.
|
|
35
|
+
> note: dexterm 0.2.2 is out (2026-11-05), open source under AGPL-3.0-only at github.com/dexterm-xyz/dexterm (https://github.com/dexterm-xyz/dexterm). It is early: trading is experimental and has known bugs. The release ships checksums, a build-provenance attestation and an SBOM, and its VirusTotal result (including flags we explain) is on the trust page (https://dexterm.xyz/trust/). It is also on npm as `dexterm-cli` (https://www.npmjs.com/package/dexterm-cli); the command it installs is still `dexterm`.
|
|
@@ -30,7 +30,7 @@ date: 2026-10-16
|
|
|
30
30
|
`ZORP · lifecycle: ignition · rank 3 · #2 of 9`.
|
|
31
31
|
- `dexterm narratives` prints the heat table. `dexterm narratives detail ID` shows one narrative, `dexterm narratives of
|
|
32
32
|
MINT` shows the narrative of one coin, and `dexterm narratives export --out FILE` writes the JSON
|
|
33
|
-
(`dexterm-narratives/1`)
|
|
33
|
+
(`dexterm-narratives/1`) for your own tools. `--at ISO` replays up to a past time.
|
|
34
34
|
- MCP tools for your analyst or your own agent: `narratives_top`, `narrative_detail`, `narrative_of` (observe tier,
|
|
35
35
|
read-only). Coin names come back wrapped as untrusted data.
|
|
36
36
|
- Watch rules: the field `narrative_rank` and the tags `narrative:ignition` / `narrative:saturated`, so a rule can fire
|