optic-agent 0.0.0-stage → 1.0.1
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/LICENSE +21 -0
- package/README.md +67 -2
- package/bin/optic.mjs +155 -0
- package/package.json +13 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OPTIC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,68 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/opticagents/optic-agent/main/brand/logo-light.png">
|
|
4
|
+
<img src="https://raw.githubusercontent.com/opticagents/optic-agent/main/brand/logo-dark.png" width="150" alt="OPTIC">
|
|
5
|
+
</picture>
|
|
6
|
+
</p>
|
|
2
7
|
|
|
3
|
-
|
|
8
|
+
<h1 align="center">optic-agent</h1>
|
|
9
|
+
|
|
10
|
+
<p align="center"><b>Balance beyond noise.</b> OPTIC in your terminal.</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://www.npmjs.com/package/optic-agent"><img src="https://img.shields.io/npm/v/optic-agent?color=111111&label=npm" alt="npm"></a>
|
|
14
|
+
<a href="https://github.com/opticagents/optic-agent"><img src="https://img.shields.io/badge/github-optic--agent-111111?logo=github" alt="GitHub"></a>
|
|
15
|
+
<a href="https://opticagents.com"><img src="https://img.shields.io/badge/web-opticagents.com-111111" alt="Website"></a>
|
|
16
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-555555" alt="MIT"></a>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
OPTIC is a portfolio agent on Robinhood Chain built on the barbell strategy. Most of a portfolio sits on the **safe side** (tokenized stocks and ETFs, USDG); a small share rides the **meme side** (Pons memecoins). Every five minutes six on-chain readings become a **market score** from 1 (panic) to 10 (euphoria). In euphoria it takes meme profit into stocks, in panic it buys memes back with safe-side money, in between it only moves on a large drift.
|
|
20
|
+
|
|
21
|
+
This package only **reads**. It holds no keys and signs nothing. Rebalancing happens in the web terminal at **[opticagents.com/app](https://opticagents.com/app)**, where your own wallet confirms every step.
|
|
22
|
+
|
|
23
|
+
## Use
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npx optic-agent # the market score, its six readings and the note
|
|
27
|
+
npx optic-agent record # the four house books and what Optic AI did
|
|
28
|
+
npx optic-agent picks # memes that pass the rug scan right now
|
|
29
|
+
npx optic-agent bar 0x… # safe side, meme side and the rebalance plan for any wallet
|
|
30
|
+
npx optic-agent watch # the score, refreshed every minute
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or install once and run `optic`:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
npm i -g optic-agent
|
|
37
|
+
optic
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`OPTIC_URL` points the CLI at another OPTIC server, for example your own: `OPTIC_URL=http://127.0.0.1:4350 optic`.
|
|
41
|
+
|
|
42
|
+
## The score
|
|
43
|
+
|
|
44
|
+
| Reading | Weight |
|
|
45
|
+
|---|---|
|
|
46
|
+
| Pons volume against its own baseline | 25% |
|
|
47
|
+
| Share of Pons volume that is buying | 25% |
|
|
48
|
+
| Pons launch rate | 10% |
|
|
49
|
+
| ETH 24 h move | 15% |
|
|
50
|
+
| SPY / QQQ token 24 h move | 10% |
|
|
51
|
+
| Tone of ETH and SPY headlines (VADER) | 15% |
|
|
52
|
+
|
|
53
|
+
Code computes the score, the rule and the picks. Claude only writes the short note under the number.
|
|
54
|
+
|
|
55
|
+
## The rule
|
|
56
|
+
|
|
57
|
+
- Score 8 or higher and the meme side over target: trim it back to target.
|
|
58
|
+
- Score 3 or lower and the meme side under target: top it up to target, spending USDG first.
|
|
59
|
+
- Anything in between: act only on a drift beyond the band, and never against the score.
|
|
60
|
+
- Memes it may buy are the busiest Pons tokens of the last 24 h, older than 6 h, still on their curve, with rug risk under 30.
|
|
61
|
+
|
|
62
|
+
## Honest limits
|
|
63
|
+
|
|
64
|
+
The score describes the last few hours; it is not a forecast. Four public paper books, opened the same day with the same money, show whether following it helps. Stock tokens follow their stocks through on-chain pools and are not shares. Not financial advice. Not affiliated with Robinhood, Uniswap or Pons.
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
MIT
|
package/bin/optic.mjs
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// optic: the OPTIC terminal for your shell. It only reads: no keys, no signing.
|
|
3
|
+
// Rebalancing a wallet is signed in the web terminal, where the wallet itself confirms each step.
|
|
4
|
+
const VERSION = "1.0.1";
|
|
5
|
+
const SERVER = (process.env.OPTIC_URL || "https://opticagents.com").replace(/\/$/, "");
|
|
6
|
+
|
|
7
|
+
const tty = (process.stdout.isTTY || !!process.env.FORCE_COLOR) && !process.env.NO_COLOR;
|
|
8
|
+
const c = (code) => (s) => (tty ? `\x1b[${code}m${s}\x1b[0m` : String(s));
|
|
9
|
+
const gold = c("38;2;230;194;154"), safe = c("38;2;157;184;236"), wild = c("38;2;255;148;102"), ok = c("38;2;134;217;168"), bad = c("38;2;255;138;114");
|
|
10
|
+
const dim = c("2"), bold = c("1");
|
|
11
|
+
const out = (s = "") => process.stdout.write(s + "\n");
|
|
12
|
+
const strip = (s) => String(s).replace(/\x1b\[[0-9;]*m/g, "");
|
|
13
|
+
const pad = (s, n, right = false) => { const g = Math.max(0, n - strip(s).length); return right ? " ".repeat(g) + s : s + " ".repeat(g); };
|
|
14
|
+
const usd = (x, d = 2) => (x == null ? "–" : "$" + Number(x).toLocaleString("en-US", { minimumFractionDigits: d, maximumFractionDigits: d }));
|
|
15
|
+
const pct = (x, d = 2) => (x == null ? "–" : (x > 0 ? "+" : "") + Number(x).toFixed(d) + "%");
|
|
16
|
+
const signed = (x, s) => (x >= 0 ? ok(s) : bad(s));
|
|
17
|
+
const ago = (t) => { if (!t) return "–"; const s = Math.round((Date.now() - t) / 1000); return s < 60 ? `${s}s ago` : s < 3600 ? `${Math.round(s / 60)}m ago` : s < 86400 ? `${Math.round(s / 3600)}h ago` : `${Math.round(s / 86400)}d ago`; };
|
|
18
|
+
const zoneCol = (k) => (k === "panic" ? safe : k === "euphoria" ? wild : (s) => s);
|
|
19
|
+
|
|
20
|
+
async function get(path) {
|
|
21
|
+
let r;
|
|
22
|
+
try { r = await fetch(SERVER + path, { headers: { "user-agent": "optic-cli/" + VERSION } }); }
|
|
23
|
+
catch { throw new Error(`Cannot reach ${SERVER}. Check your connection, or set OPTIC_URL.`); }
|
|
24
|
+
const j = await r.json().catch(() => ({}));
|
|
25
|
+
if (!r.ok) throw new Error(j.error || `HTTP ${r.status}`);
|
|
26
|
+
return j;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function head(title) {
|
|
30
|
+
out();
|
|
31
|
+
out(` ${gold("◠")} ${bold("OPTIC")} ${dim("·")} ${title}`);
|
|
32
|
+
out(dim(" " + "─".repeat(58)));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// a 1–10 scale with the needle where the score sits
|
|
36
|
+
function meter(score, w = 46) {
|
|
37
|
+
const i = Math.round(((score - 1) / 9) * (w - 1));
|
|
38
|
+
let bar = "";
|
|
39
|
+
for (let k = 0; k < w; k++) { const ch = k === i ? "●" : "─"; const at = k / (w - 1); bar += k === i ? bold(ch) : at < 0.23 ? safe(ch) : at > 0.77 ? wild(ch) : dim(ch); }
|
|
40
|
+
return ` ${bar}\n ${dim(pad("1 panic", 12) + pad("3", 12) + pad("8", 12) + pad("10", 10, true))}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const PART = { memeVolume: "Meme volume", memeBuyShare: "Meme buying", launches: "New launches", eth: "ETH", stocks: "Stock tokens", news: "Headlines" };
|
|
44
|
+
function partVal(k, p) {
|
|
45
|
+
if (p.value == null) return "–";
|
|
46
|
+
return k === "news" ? (p.value > 0 ? "+" : "") + p.value.toFixed(2) : k === "memeVolume" ? `${p.value} ETH/h` : k === "memeBuyShare" ? `${p.value}%` : k === "launches" ? `${p.value}/h` : pct(p.value, 2);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function score() {
|
|
50
|
+
const s = await get("/api/state");
|
|
51
|
+
head("market score");
|
|
52
|
+
if (s.score == null) { out(" The first score lands a few minutes after the server starts."); return out(); }
|
|
53
|
+
out();
|
|
54
|
+
out(` ${bold(gold(s.score.toFixed(1)))}${dim("/10")} ${zoneCol(s.zone.key)(s.zone.label.toUpperCase())} ${dim("updated " + ago(s.scoreAt))}`);
|
|
55
|
+
out();
|
|
56
|
+
out(meter(s.score));
|
|
57
|
+
out();
|
|
58
|
+
out(" " + (s.why || `${s.zone.label}: ${s.zone.act}.`));
|
|
59
|
+
out();
|
|
60
|
+
out(dim(" READING VALUE HEAT WEIGHT"));
|
|
61
|
+
for (const [k, p] of Object.entries(s.parts || {})) {
|
|
62
|
+
const h = Math.round((p.heat || 0) * 10), heat = "▮".repeat(h) + dim("▯".repeat(10 - h));
|
|
63
|
+
out(` ${pad(PART[k] || k, 19)}${pad(partVal(k, p), 16)}${p.heat > 0.6 ? wild(heat) : p.heat < 0.4 ? safe(heat) : heat} ${dim(Math.round(s.weights[k] * 100) + "%")}`);
|
|
64
|
+
}
|
|
65
|
+
out();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function record() {
|
|
69
|
+
const s = await get("/api/state"), H = s.house || {};
|
|
70
|
+
head("the record");
|
|
71
|
+
out(dim(` Four paper books of ${(s.start || 1000).toLocaleString()} USDG${s.houseStarted ? ", opened " + new Date(s.houseStarted).toLocaleDateString() : ""}. Live quotes, all fees in.`));
|
|
72
|
+
out();
|
|
73
|
+
out(dim(" BOOK RETURN EQUITY RULE"));
|
|
74
|
+
for (const [k, b] of Object.entries(H)) out(` ${pad(k === "barbell" ? gold(b.name) : b.name, 18)}${pad(signed(b.ret, pct(b.ret)), 12)}${pad(usd(b.equity), 12)}${dim(b.note)}`);
|
|
75
|
+
const t = H.barbell?.trades || [];
|
|
76
|
+
if (t.length) {
|
|
77
|
+
out();
|
|
78
|
+
out(dim(` WHAT ${(H.barbell.name || "").toUpperCase()} DID`));
|
|
79
|
+
for (const x of t.slice(0, 8)) out(` ${pad(dim(ago(x.t)), 10)}${pad(x.side === "buy" ? ok(x.side) : x.side === "sell" ? bad(x.side) : dim(x.side), 7)}${pad(usd(x.usd), 12)}${x.what} ${dim(x.why)}`);
|
|
80
|
+
}
|
|
81
|
+
out();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function picks() {
|
|
85
|
+
const s = await get("/api/state");
|
|
86
|
+
head("memes it may buy now");
|
|
87
|
+
out(dim(" Busiest Pons tokens of the last 24 h, older than 6 h, still on their curve, rug risk under 30."));
|
|
88
|
+
out();
|
|
89
|
+
if (!s.picks?.length) out(" No memecoin passes the scan right now.");
|
|
90
|
+
else {
|
|
91
|
+
out(dim(" TOKEN 24H VOLUME RUG RISK CA"));
|
|
92
|
+
for (const p of s.picks) out(` ${pad(gold("$" + p.symbol), 16)}${pad(p.volEth + " ETH", 14)}${pad(p.risk + "/100", 11)}${dim(p.token)}`);
|
|
93
|
+
}
|
|
94
|
+
out(dim(`\n scanned ${ago(s.picksAt)}`));
|
|
95
|
+
out();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function bar(address, target) {
|
|
99
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(address || "")) throw new Error("Usage: optic bar <0x wallet address> [--target 20]");
|
|
100
|
+
const q = `/api/look?address=${address}` + (target != null ? `&target=${target}` : "");
|
|
101
|
+
if (tty) process.stdout.write(dim(" reading the wallet from the chain…") + "\r");
|
|
102
|
+
const [v, s] = await Promise.all([get(q), get("/api/state")]);
|
|
103
|
+
if (tty) process.stdout.write(" ".repeat(40) + "\r");
|
|
104
|
+
const h = v.holdings, total = h.safeUsd + h.memeUsd, share = total ? h.memeUsd / total : 0, tgt = v.policy.target;
|
|
105
|
+
head(`bar of ${address.slice(0, 6)}…${address.slice(-4)}`);
|
|
106
|
+
out();
|
|
107
|
+
out(` ${bold(usd(total))} ${dim("on the bar")} ${dim("score")} ${s.score?.toFixed(1) ?? "–"} ${zoneCol(s.zone?.key)(s.zone?.label || "")}`);
|
|
108
|
+
out();
|
|
109
|
+
const w = 46, cut = Math.round((1 - share) * w), tm = Math.round((1 - tgt) * w);
|
|
110
|
+
let line = ""; for (let k = 0; k < w; k++) line += k === tm ? gold("┃") : !total ? dim("─") : k < cut ? safe("█") : wild("█");
|
|
111
|
+
out(" " + line);
|
|
112
|
+
out(` ${safe(total ? `safe ${((1 - share) * 100).toFixed(1)}%` : "safe –")}${" ".repeat(8)}${dim(`target ${Math.round(tgt * 100)}% meme`)}${" ".repeat(8)}${wild(`meme ${(share * 100).toFixed(1)}%`)}`);
|
|
113
|
+
out();
|
|
114
|
+
out(safe(" SAFE SIDE"));
|
|
115
|
+
if (!h.safe.length) out(dim(" no stock tokens or USDG"));
|
|
116
|
+
for (const x of h.safe) out(` ${pad(x.sym, 10)}${pad(x.amount.toLocaleString("en-US", { maximumFractionDigits: 4 }), 16, true)}${pad(usd(x.usd), 14, true)}`);
|
|
117
|
+
out(wild("\n MEME SIDE"));
|
|
118
|
+
if (!h.meme.length) out(dim(" no Pons memecoin traded in the last week"));
|
|
119
|
+
for (const x of h.meme) out(` ${pad("$" + x.symbol, 26)}${pad(usd(x.usd), 14, true)}`);
|
|
120
|
+
out(dim(`\n ETH for gas: ${h.eth.amount.toFixed(5)} (${usd(h.eth.usd)})`));
|
|
121
|
+
out();
|
|
122
|
+
const p = v.plan;
|
|
123
|
+
const title = p.action === "trim" ? `Take ${usd(p.usd)} of meme profit into the safe side` : p.action === "topup" ? `Buy ${usd(p.usd)} of memes back with safe-side money` : "Hold. The bar is where it should be.";
|
|
124
|
+
out(` ${gold("THE PLAN")} ${bold(title)}`);
|
|
125
|
+
out(" " + dim(p.reason));
|
|
126
|
+
p.steps.forEach((t, i) => out(` ${gold(String(i + 1))} ${t}`));
|
|
127
|
+
if (p.steps.length) out(`\n Sign these steps with your wallet at ${gold(SERVER + "/app#bar")}`);
|
|
128
|
+
out();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function help() {
|
|
132
|
+
out();
|
|
133
|
+
out(` ${gold("◠")} ${bold("OPTIC")} ${dim("v" + VERSION + " · balance beyond noise")}`);
|
|
134
|
+
out();
|
|
135
|
+
out(` ${bold("optic")} the live score, its six readings and the note`);
|
|
136
|
+
out(` ${bold("optic record")} the four house books and what Optic did`);
|
|
137
|
+
out(` ${bold("optic picks")} memes that pass the rug scan right now`);
|
|
138
|
+
out(` ${bold("optic bar")} ${dim("<0x…>")} holdings and the rebalance plan for a wallet`);
|
|
139
|
+
out(` ${bold("optic watch")} the score, refreshed every minute`);
|
|
140
|
+
out();
|
|
141
|
+
out(dim(" Read-only. No keys, no signing: trades are signed in the web terminal."));
|
|
142
|
+
out(dim(` Server: ${SERVER} (override with OPTIC_URL)`));
|
|
143
|
+
out();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function watch() {
|
|
147
|
+
const tick = async () => { if (tty) process.stdout.write("\x1b[2J\x1b[H"); try { await score(); } catch (e) { out(bad(" " + e.message)); } out(dim(" refreshing every minute · ctrl+c to stop")); };
|
|
148
|
+
await tick(); setInterval(tick, 60_000);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const [cmd = "score", ...rest] = process.argv.slice(2);
|
|
152
|
+
const flag = (n) => { const i = rest.indexOf("--" + n); return i < 0 ? null : rest[i + 1]; };
|
|
153
|
+
const run = { score, record, picks, watch, bar: () => bar(rest[0], flag("target")), help, "--help": help, "-h": help, "--version": () => out(VERSION), "-v": () => out(VERSION) }[cmd];
|
|
154
|
+
if (!run) { out(bad(` Unknown command: ${cmd}`)); help(); process.exit(1); }
|
|
155
|
+
Promise.resolve().then(run).catch((e) => { out(bad("\n " + e.message + "\n")); process.exit(1); });
|
package/package.json
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "optic-agent",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
}
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "OPTIC in your terminal: the live market score, the four house books, the meme picks and a read-only look at any wallet's safe side and meme side on Robinhood Chain.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": { "optic": "bin/optic.mjs" },
|
|
7
|
+
"files": ["bin", "README.md", "LICENSE"],
|
|
8
|
+
"engines": { "node": ">=18" },
|
|
9
|
+
"homepage": "https://opticagents.com",
|
|
10
|
+
"repository": { "type": "git", "url": "git+https://github.com/opticagents/optic-agent.git" },
|
|
11
|
+
"bugs": { "url": "https://github.com/opticagents/optic-agent/issues" },
|
|
12
|
+
"keywords": ["optic", "robinhood-chain", "pons", "memecoin", "portfolio", "agent", "cli", "rebalance", "barbell", "uniswap", "usdg"],
|
|
13
|
+
"author": "OPTIC (https://opticagents.com)",
|
|
14
|
+
"license": "MIT"
|
|
15
|
+
}
|