@keelage/mcp 0.1.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 +100 -0
- package/bin/keelage.mjs +54 -0
- package/data/thresholds.json +26 -0
- package/data/track-record.json +61 -0
- package/lib/calc.mjs +338 -0
- package/lib/format.mjs +166 -0
- package/lib/gate.mjs +638 -0
- package/lib/holders.mjs +67 -0
- package/lib/io.mjs +43 -0
- package/lib/jev.mjs +104 -0
- package/lib/record.mjs +19 -0
- package/lib/render.mjs +56 -0
- package/lib/state.mjs +17 -0
- package/lib/template.mjs +22 -0
- package/lib/templates.json +100 -0
- package/lib/x-meta.mjs +214 -0
- package/package.json +45 -0
- package/server/core.mjs +114 -0
- package/server/hosted-io.mjs +9 -0
- package/server/mcp.mjs +25 -0
package/lib/format.mjs
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// format.mjs - one number grammar and one text card for a token. Plain text, no markup.
|
|
2
|
+
//
|
|
3
|
+
// Rule enforced here: unknown is never zero. A value that was never read renders "unknown", never "$0".
|
|
4
|
+
// Every formatter returns UNK for null, undefined, '' and NaN, so no caller can print a placeholder zero.
|
|
5
|
+
import { baseBand } from './calc.mjs';
|
|
6
|
+
import { xLine } from './x-meta.mjs';
|
|
7
|
+
import { jevLine } from './jev.mjs';
|
|
8
|
+
|
|
9
|
+
export const UNK = 'unknown';
|
|
10
|
+
export const LOCALE = 'en-US';
|
|
11
|
+
|
|
12
|
+
// Address key normalization: 0x addresses are stored lowercase everywhere.
|
|
13
|
+
export const keyNorm = (chain, a) => `${chain}:${/^0x/i.test(a) ? String(a).toLowerCase() : a}`;
|
|
14
|
+
|
|
15
|
+
const bad = (n) => n === null || n === undefined || n === '' || !Number.isFinite(Number(n));
|
|
16
|
+
|
|
17
|
+
export const usd = (n, dp = 0) => bad(n) ? UNK
|
|
18
|
+
: `$${Number(n).toLocaleString(LOCALE, { minimumFractionDigits: dp, maximumFractionDigits: dp })}`;
|
|
19
|
+
|
|
20
|
+
// Compact form for market cap and volume where exact dollars are noise: $1.2M, $350k.
|
|
21
|
+
export const usdC = (n) => {
|
|
22
|
+
if (bad(n)) return UNK;
|
|
23
|
+
const v = Number(n), a = Math.abs(v), sign = v < 0 ? '-' : '';
|
|
24
|
+
if (a >= 1e9) return `${sign}$${(a / 1e9).toFixed(a >= 1e10 ? 0 : 1)}B`;
|
|
25
|
+
if (a >= 1e6) return `${sign}$${(a / 1e6).toFixed(a >= 1e7 ? 0 : 1)}M`;
|
|
26
|
+
if (a >= 1e3) return `${sign}$${Math.round(a / 1e3)}k`;
|
|
27
|
+
return `${sign}$${a.toFixed(0)}`;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Token prices span many orders of magnitude, so a fixed number of decimals is useless at one end.
|
|
31
|
+
export const price = (n) => {
|
|
32
|
+
if (bad(n)) return UNK;
|
|
33
|
+
const v = Number(n), a = Math.abs(v);
|
|
34
|
+
if (a === 0) return '$0';
|
|
35
|
+
if (a >= 1) return `$${v.toFixed(2)}`;
|
|
36
|
+
if (a >= 0.01) return `$${v.toFixed(4)}`;
|
|
37
|
+
return `$${Number(v.toPrecision(4))}`;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const pct = (n, dp = 1) => bad(n) ? UNK
|
|
41
|
+
: `${Number(n) > 0 ? '+' : ''}${Number(n).toFixed(dp)}%`;
|
|
42
|
+
|
|
43
|
+
// Unsigned percentage, for shares (top-10 28%), not deltas.
|
|
44
|
+
export const pctAbs = (n, dp = 0) => bad(n) ? UNK : `${Number(n).toFixed(dp)}%`;
|
|
45
|
+
|
|
46
|
+
export const qty = (n, dp = 0) => bad(n) ? UNK
|
|
47
|
+
: Number(n).toLocaleString(LOCALE, { maximumFractionDigits: dp });
|
|
48
|
+
|
|
49
|
+
export const age = (d) => bad(d) ? UNK : `${Math.round(Number(d))}d`;
|
|
50
|
+
|
|
51
|
+
// Minutes-since -> "3h" / "45m" / "2d".
|
|
52
|
+
export const ago = (isoOrMs) => {
|
|
53
|
+
if (!isoOrMs) return UNK;
|
|
54
|
+
const t = typeof isoOrMs === 'number' ? isoOrMs : Date.parse(isoOrMs);
|
|
55
|
+
if (!Number.isFinite(t)) return UNK;
|
|
56
|
+
const m = Math.max(0, Math.round((Date.now() - t) / 60_000));
|
|
57
|
+
if (m < 60) return `${m}m`;
|
|
58
|
+
if (m < 1440) return `${Math.round(m / 60)}h`;
|
|
59
|
+
return `${Math.round(m / 1440)}d`;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// A pipe-joined field line. Fields whose value is null, undefined or '' are dropped whole, so a line
|
|
63
|
+
// never contains a bare "null".
|
|
64
|
+
export const fields = (pairs) => pairs
|
|
65
|
+
.filter(([, v]) => v !== null && v !== undefined && v !== '')
|
|
66
|
+
.map(([k, v]) => (k ? `${k} ${v}` : String(v)))
|
|
67
|
+
.join(' | ');
|
|
68
|
+
|
|
69
|
+
// Public pages for a token on Robinhood Chain.
|
|
70
|
+
export function links(address) {
|
|
71
|
+
if (!address) return [];
|
|
72
|
+
return [
|
|
73
|
+
`https://dexscreener.com/robinhood/${address}`,
|
|
74
|
+
`https://www.geckoterminal.com/robinhood/tokens/${address}`,
|
|
75
|
+
`https://robinhoodchain.blockscout.com/token/${address}`,
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const shortAddr = (a) => {
|
|
80
|
+
const s = String(a ?? '');
|
|
81
|
+
return s.length > 16 ? `${s.slice(0, 4)}…${s.slice(-4)}` : s;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Base band in words, then the number. The band is what predicts, so it leads.
|
|
85
|
+
export function baseLabel(o) {
|
|
86
|
+
const d = o.baseEntry?.distPct ?? o.distFromLow30Pct;
|
|
87
|
+
const b = baseBand(d);
|
|
88
|
+
return d == null ? b.label : `${b.label} (${pctAbs(d)} above 30d low)`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// The token card: fixed field order, every line optional as a whole, no line ever containing null.
|
|
92
|
+
// Gate state vocabulary is exactly PASS / FAIL / UNVERIFIED.
|
|
93
|
+
export function tokenCard(o) {
|
|
94
|
+
const g = o.gates || {};
|
|
95
|
+
const gateState = g.gateErr ? 'UNVERIFIED' : (g.pass ? 'PASS' : 'FAIL');
|
|
96
|
+
const lines = [];
|
|
97
|
+
const id = (s) => s;
|
|
98
|
+
|
|
99
|
+
const head = fields([
|
|
100
|
+
['score', o.score ?? null], // score ranks, never admits
|
|
101
|
+
['tier', o.tier || null],
|
|
102
|
+
]);
|
|
103
|
+
lines.push(`${o.symbol || shortAddr(o.address)}${head ? ` | ${head}` : ''}`);
|
|
104
|
+
|
|
105
|
+
lines.push(fields([
|
|
106
|
+
['liq', o.liqUsd == null ? null : usd(o.liqUsd)],
|
|
107
|
+
['vol24', o.vol24 == null ? null : usd(o.vol24)],
|
|
108
|
+
['mcap', o.mcapUsd == null ? null : usdC(o.mcapUsd)],
|
|
109
|
+
['age', o.ageDays == null ? null : age(o.ageDays)],
|
|
110
|
+
['24h', o.priceChange24 == null ? null : pct(o.priceChange24)],
|
|
111
|
+
['', o.athDrawdown == null ? null : `${pctAbs(o.athDrawdown)} below ATH`],
|
|
112
|
+
['', baseLabel(o)],
|
|
113
|
+
]));
|
|
114
|
+
|
|
115
|
+
lines.push(fields([
|
|
116
|
+
['gates', gateState],
|
|
117
|
+
['', g.gateErr ? `provider ${String(g.gateErr).slice(0, 60)}` : null],
|
|
118
|
+
['top10', g.top10Pct == null ? null : `${pctAbs(g.top10Pct)}${g.top10Source ? ` (${g.top10Source})` : ''}`],
|
|
119
|
+
['', g.maxSinglePct == null ? null : `largest wallet ${pctAbs(g.maxSinglePct)}`],
|
|
120
|
+
['creator', g.creatorPct == null ? null : pctAbs(g.creatorPct)],
|
|
121
|
+
['holders', g.holderCount == null ? null : qty(g.holderCount)],
|
|
122
|
+
]));
|
|
123
|
+
|
|
124
|
+
// What the chain says about the contract itself.
|
|
125
|
+
if (g.source === 'blockscout') {
|
|
126
|
+
const priv = Object.keys(g.privileges || {}).filter((k) => k !== 'live' && k !== 'maxLimits');
|
|
127
|
+
lines.push(fields([
|
|
128
|
+
['source', g.verified ? 'verified' : 'UNVERIFIED'],
|
|
129
|
+
['template', g.template ? `${g.template}${g.templateVerdict && g.templateVerdict !== 'clean' ? ` (${g.templateVerdict})` : ''}` : null],
|
|
130
|
+
['', g.implName ? `via ${g.implName} implementation` : null],
|
|
131
|
+
['privileges', priv.length ? `${priv.join('+')}${g.ownerRenounced === true ? ', owner renounced' : g.ownerRenounced === false ? ', OWNER ACTIVE' : ', owner unverified'}` : 'none'],
|
|
132
|
+
['tax code', g.taxCode == null ? null : (g.taxCode ? 'PRESENT' : 'none')],
|
|
133
|
+
['sells 24h', g.sellsClear == null ? null : (g.sellsClear ? `clearing (${g.sells24})` : 'NONE')],
|
|
134
|
+
['liq', g.liqDropPct == null ? (g.liqPoints != null ? `history ${g.liqPoints}d` : null) : `${pctAbs(g.liqDropPct)} below its ${g.liqPoints}d peak`],
|
|
135
|
+
]));
|
|
136
|
+
if (g.reasons?.length) lines.push(`FAIL: ${g.reasons.join(' | ').slice(0, 300)}`);
|
|
137
|
+
if (g.caps?.length) lines.push(`unknown (caps size): ${g.caps.join(' | ').slice(0, 200)}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// GoPlus corroboration, only where it has a record.
|
|
141
|
+
lines.push(fields([
|
|
142
|
+
['goplus', g.goplusErr ? 'no record' : (g.honeypot == null && g.buyTax == null && g.lpMode == null ? null : 'record')],
|
|
143
|
+
['honeypot', g.honeypot == null ? null : (g.honeypot ? 'YES' : 'no')],
|
|
144
|
+
['tax', g.buyTax == null && g.sellTax == null ? null : `${g.buyTax ?? '?'}/${g.sellTax ?? '?'}`],
|
|
145
|
+
['LP', g.lpMode == null ? null : `${g.lpMode === 'cl' ? 'V3/V4 positions' : g.lpMode}${g.lpHolders != null ? `, ${g.lpHolders} holder(s), top ${pctAbs(g.topLpPct)}` : ''}${g.creatorLpPct ? `, creator ${pctAbs(g.creatorLpPct)}` : ''}`],
|
|
146
|
+
]));
|
|
147
|
+
|
|
148
|
+
// A paid takeover claim is a caution, not a credential: claimed tokens read -58% median / 11% positive
|
|
149
|
+
// at 7 days against -13% / 37% unclaimed (n=155 vs 858).
|
|
150
|
+
const flags = [];
|
|
151
|
+
if (o.takeoverClaim) flags.push(`TAKEOVER CLAIM x${o.takeoverClaim.count} (latest ${o.takeoverClaim.latest}) - CAUTION: historically the worst-performing bucket`);
|
|
152
|
+
if (flags.length) lines.push(flags.join(' | '));
|
|
153
|
+
|
|
154
|
+
// X and Jev lines are context only: never a gate, never a rank key.
|
|
155
|
+
const xl = xLine(o.xMeta, id);
|
|
156
|
+
if (xl) lines.push(xl);
|
|
157
|
+
const jl = jevLine(o.jev, id);
|
|
158
|
+
if (jl) lines.push(jl);
|
|
159
|
+
|
|
160
|
+
if (o.sizeLine) lines.push(o.sizeLine);
|
|
161
|
+
|
|
162
|
+
lines.push(o.address);
|
|
163
|
+
lines.push(...links(o.address));
|
|
164
|
+
|
|
165
|
+
return lines.filter(Boolean).join('\n');
|
|
166
|
+
}
|