@usenami/signer-mcp 0.1.0
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/CHANGELOG.md +36 -0
- package/LICENSE +21 -0
- package/README.md +212 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +197 -0
- package/dist/index.js.map +1 -0
- package/dist/lib.d.ts +129 -0
- package/dist/lib.js +281 -0
- package/dist/lib.js.map +1 -0
- package/dist/parsers/asterdex.d.ts +35 -0
- package/dist/parsers/asterdex.js +79 -0
- package/dist/parsers/asterdex.js.map +1 -0
- package/dist/parsers/binance.d.ts +32 -0
- package/dist/parsers/binance.js +72 -0
- package/dist/parsers/binance.js.map +1 -0
- package/dist/parsers/index.d.ts +16 -0
- package/dist/parsers/index.js +25 -0
- package/dist/parsers/index.js.map +1 -0
- package/dist/parsers/okx.d.ts +34 -0
- package/dist/parsers/okx.js +104 -0
- package/dist/parsers/okx.js.map +1 -0
- package/dist/parsers/types.d.ts +62 -0
- package/dist/parsers/types.js +10 -0
- package/dist/parsers/types.js.map +1 -0
- package/package.json +65 -0
- package/server.json +62 -0
package/dist/lib.js
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core handlers and helpers for @usenami/signer-mcp.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from index.ts so unit tests can exercise the gateway-call paths
|
|
5
|
+
* without booting a real MCP transport. index.ts wires these into MCP tools.
|
|
6
|
+
*/
|
|
7
|
+
export const PACKAGE_VERSION = "0.1.0";
|
|
8
|
+
export const DEFAULT_FETCH_TIMEOUT_MS = 30_000;
|
|
9
|
+
export const STATIC_VENUES = [
|
|
10
|
+
{
|
|
11
|
+
venue: "binance",
|
|
12
|
+
asset_class: "perp",
|
|
13
|
+
auth_scheme: "hmac_sha256",
|
|
14
|
+
notes: "Binance USD-M futures via REST. v0 limited to testnet until pilot " +
|
|
15
|
+
"graduates. Symbol format: BTCUSDT (no slash).",
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
venue: "okx",
|
|
19
|
+
asset_class: "perp",
|
|
20
|
+
auth_scheme: "hmac_sha256",
|
|
21
|
+
notes: "OKX perpetual swap via REST. v0 limited to testnet. Symbol format: " +
|
|
22
|
+
"BTC-USDT-SWAP.",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
venue: "asterdex",
|
|
26
|
+
asset_class: "perp",
|
|
27
|
+
auth_scheme: "eip712",
|
|
28
|
+
network: "bsc",
|
|
29
|
+
notes: "Asterdex on-chain perp. Uses Asterdex platform-controlled API wallet " +
|
|
30
|
+
"(narrow per-asset caps enforced by Signer policy).",
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
// ── Gateway HTTP ──
|
|
34
|
+
export class GatewayError extends Error {
|
|
35
|
+
status;
|
|
36
|
+
body;
|
|
37
|
+
endpoint;
|
|
38
|
+
constructor(status, body, endpoint) {
|
|
39
|
+
super(`gateway ${endpoint} failed (${status}): ${body.slice(0, 240)}`);
|
|
40
|
+
this.status = status;
|
|
41
|
+
this.body = body;
|
|
42
|
+
this.endpoint = endpoint;
|
|
43
|
+
this.name = "GatewayError";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Single chokepoint for all gateway HTTP. Tests inject a `fetchImpl` and
|
|
48
|
+
* assert against the dispatched request.
|
|
49
|
+
*
|
|
50
|
+
* Edge cases handled:
|
|
51
|
+
* - Missing token when authRequired → thrown with actionable message.
|
|
52
|
+
* - Empty 200 OK body → returned as undefined (rare but possible on some
|
|
53
|
+
* venues' cancel-already-filled paths).
|
|
54
|
+
* - Non-2xx → GatewayError carries status + body for the agent to inspect.
|
|
55
|
+
* - Timeout → AbortController + clearTimeout via try/finally.
|
|
56
|
+
*/
|
|
57
|
+
export async function callGateway(path, opts, cfg) {
|
|
58
|
+
const method = opts.method ?? "GET";
|
|
59
|
+
const hasToken = Boolean(cfg.apiToken && cfg.apiToken.length > 0);
|
|
60
|
+
if (opts.authRequired && !hasToken) {
|
|
61
|
+
throw new Error(`SIGNER_API_TOKEN is required to call ${path}. Set it in your MCP ` +
|
|
62
|
+
`client config — see README. (Issue tokens at https://usenami.io/signer.)`);
|
|
63
|
+
}
|
|
64
|
+
const url = `${cfg.gatewayUrl.replace(/\/+$/, "")}${path}`;
|
|
65
|
+
const timeoutMs = cfg.fetchTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
66
|
+
const fetchFn = cfg.fetchImpl ?? fetch;
|
|
67
|
+
const controller = new AbortController();
|
|
68
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
69
|
+
try {
|
|
70
|
+
const headers = {
|
|
71
|
+
Accept: "application/json",
|
|
72
|
+
"User-Agent": `@usenami/signer-mcp@${PACKAGE_VERSION}`,
|
|
73
|
+
};
|
|
74
|
+
if (hasToken)
|
|
75
|
+
headers.Authorization = `Bearer ${cfg.apiToken}`;
|
|
76
|
+
let serialized;
|
|
77
|
+
if (opts.body !== undefined) {
|
|
78
|
+
serialized = JSON.stringify(opts.body);
|
|
79
|
+
headers["Content-Type"] = "application/json";
|
|
80
|
+
}
|
|
81
|
+
const res = await fetchFn(url, {
|
|
82
|
+
method,
|
|
83
|
+
headers,
|
|
84
|
+
body: serialized,
|
|
85
|
+
signal: controller.signal,
|
|
86
|
+
});
|
|
87
|
+
const text = await res.text();
|
|
88
|
+
if (!res.ok)
|
|
89
|
+
throw new GatewayError(res.status, text, path);
|
|
90
|
+
return text.length === 0 ? undefined : JSON.parse(text);
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function toolJson(value) {
|
|
97
|
+
// Guard: JSON.stringify(undefined) returns undefined (not a string), which
|
|
98
|
+
// breaks downstream JSON.parse. Coerce undefined → null so empty 200-OK
|
|
99
|
+
// gateway responses still produce a valid text body.
|
|
100
|
+
const safe = value === undefined ? null : value;
|
|
101
|
+
return {
|
|
102
|
+
content: [{ type: "text", text: JSON.stringify(safe, null, 2) }],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export function toolError(message, hint) {
|
|
106
|
+
const payload = JSON.stringify(hint === undefined ? { error: message } : { error: message, hint }, null, 2);
|
|
107
|
+
return { content: [{ type: "text", text: payload }], isError: true };
|
|
108
|
+
}
|
|
109
|
+
function isSignedRequest(x) {
|
|
110
|
+
// Hardened: reject primitives (truthy strings/bools still match !!obj),
|
|
111
|
+
// reject null headers (`typeof null === "object"`), require string venue.
|
|
112
|
+
if (x === null || typeof x !== "object")
|
|
113
|
+
return false;
|
|
114
|
+
const obj = x;
|
|
115
|
+
return (typeof obj.venue === "string" &&
|
|
116
|
+
typeof obj.method === "string" &&
|
|
117
|
+
typeof obj.url === "string" &&
|
|
118
|
+
obj.headers !== null &&
|
|
119
|
+
typeof obj.headers === "object");
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Submit a single signed request to a venue and return parsed JSON (or text
|
|
123
|
+
* if the response isn't JSON). Errors include the venue endpoint + status so
|
|
124
|
+
* the agent can reason about which side failed.
|
|
125
|
+
*/
|
|
126
|
+
export async function submitSignedRequest(req, fetchImpl, timeoutMs = DEFAULT_FETCH_TIMEOUT_MS) {
|
|
127
|
+
const fetchFn = fetchImpl ?? fetch;
|
|
128
|
+
const controller = new AbortController();
|
|
129
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
130
|
+
try {
|
|
131
|
+
const res = await fetchFn(req.url, {
|
|
132
|
+
method: req.method,
|
|
133
|
+
headers: req.headers,
|
|
134
|
+
body: req.body,
|
|
135
|
+
signal: controller.signal,
|
|
136
|
+
});
|
|
137
|
+
const text = await res.text();
|
|
138
|
+
if (!res.ok) {
|
|
139
|
+
throw new Error(`venue ${req.venue} ${req.method} ${req.url.split("?")[0]} failed ` +
|
|
140
|
+
`(${res.status}): ${text.slice(0, 240)}`);
|
|
141
|
+
}
|
|
142
|
+
if (text.length === 0)
|
|
143
|
+
return null;
|
|
144
|
+
try {
|
|
145
|
+
return JSON.parse(text);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Some venues return non-JSON on success (rare). Pass through as string.
|
|
149
|
+
return text;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
clearTimeout(timer);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Submit a "bundle" — either a single SignedRequest or a map of named
|
|
158
|
+
* SignedRequests (`{balance: ..., positions: ...}`). Parallel for composite,
|
|
159
|
+
* returns the same shape with raw venue responses in place of each request.
|
|
160
|
+
*/
|
|
161
|
+
export async function submitSignedBundle(bundle, fetchImpl, timeoutMs) {
|
|
162
|
+
if (isSignedRequest(bundle)) {
|
|
163
|
+
return submitSignedRequest(bundle, fetchImpl, timeoutMs);
|
|
164
|
+
}
|
|
165
|
+
// Composite — walk own keys, submit each SignedRequest in parallel.
|
|
166
|
+
if (bundle === null || typeof bundle !== "object") {
|
|
167
|
+
throw new Error("Unexpected /account response shape: not a SignedRequest and not a " +
|
|
168
|
+
"composite object. Gateway must return {method,url,headers} or " +
|
|
169
|
+
"{key: {method,url,headers}, ...}.");
|
|
170
|
+
}
|
|
171
|
+
const obj = bundle;
|
|
172
|
+
const entries = Object.entries(obj);
|
|
173
|
+
const requests = entries.filter(([, v]) => isSignedRequest(v));
|
|
174
|
+
if (requests.length === 0) {
|
|
175
|
+
// No signed requests at all — pass through (e.g. asterdex indexer
|
|
176
|
+
// payload may be returned directly by the gateway, not as a signed req).
|
|
177
|
+
return obj;
|
|
178
|
+
}
|
|
179
|
+
const results = await Promise.all(requests.map(async ([key, req]) => {
|
|
180
|
+
const resp = await submitSignedRequest(req, fetchImpl, timeoutMs);
|
|
181
|
+
return [key, resp];
|
|
182
|
+
}));
|
|
183
|
+
const out = {};
|
|
184
|
+
// Preserve any non-request fields (e.g. venue label).
|
|
185
|
+
for (const [k, v] of entries) {
|
|
186
|
+
if (!isSignedRequest(v))
|
|
187
|
+
out[k] = v;
|
|
188
|
+
}
|
|
189
|
+
for (const [k, v] of results)
|
|
190
|
+
out[k] = v;
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
// ── Tool handlers (pure logic, no MCP coupling) ──
|
|
194
|
+
export async function handleListVenues() {
|
|
195
|
+
return toolJson({ venues: STATIC_VENUES, count: STATIC_VENUES.length });
|
|
196
|
+
}
|
|
197
|
+
export async function handleGetAttestation(cfg) {
|
|
198
|
+
try {
|
|
199
|
+
const data = await callGateway("/attestation", {}, cfg);
|
|
200
|
+
return toolJson(data);
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
const e = err;
|
|
204
|
+
return toolError(e.message, "If the gateway is unreachable, list_venues still works (static " +
|
|
205
|
+
"manifest, no network). Verify SIGNER_GATEWAY_URL.");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Option-A flow for /account:
|
|
210
|
+
* 1. POST gateway /account/<venue> → returns SignedRequest bundle
|
|
211
|
+
* 2. submitSignedBundle → fetch venue with signed headers
|
|
212
|
+
* 3. dispatch raw response to venue-specific parser
|
|
213
|
+
* 4. return NormalizedAccount
|
|
214
|
+
*
|
|
215
|
+
* If the gateway response is NOT a SignedRequest (e.g. asterdex indexer
|
|
216
|
+
* returns parsed data directly), we feed the raw payload to the parser
|
|
217
|
+
* unchanged — parser is tolerant of both shapes.
|
|
218
|
+
*/
|
|
219
|
+
export async function handleGetAccount(cfg, args, getParser) {
|
|
220
|
+
const parser = getParser(args.venue);
|
|
221
|
+
if (parser === undefined) {
|
|
222
|
+
return toolError(`No account parser registered for venue '${args.venue}'.`, "Call list_venues to see supported venues.");
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const signed = await callGateway(`/account/${encodeURIComponent(args.venue)}`, { authRequired: true }, cfg);
|
|
226
|
+
const rawVenueResponse = await submitSignedBundle(signed, cfg.fetchImpl, cfg.fetchTimeoutMs);
|
|
227
|
+
const normalized = parser(rawVenueResponse);
|
|
228
|
+
return toolJson(normalized);
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
return toolError(err.message);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Option-A flow for /sign/order:
|
|
236
|
+
* 1. POST gateway /sign/order with intent body → returns SignedRequest
|
|
237
|
+
* 2. submitSignedRequest → fetch venue, return raw venue receipt
|
|
238
|
+
* 3. agent sees venue's native response (order_id format etc)
|
|
239
|
+
*
|
|
240
|
+
* No parser layer here — order receipts are per-venue and we don't normalize
|
|
241
|
+
* them in v0. Agent inspects raw response to decide next action.
|
|
242
|
+
*/
|
|
243
|
+
export async function handlePlaceOrder(cfg, args) {
|
|
244
|
+
if (args.type === "limit" && args.price === undefined) {
|
|
245
|
+
return toolError("price is required when type=limit", "For market orders, omit price and set type=market.");
|
|
246
|
+
}
|
|
247
|
+
// type=market with price set is NOT rejected — some venues silently ignore,
|
|
248
|
+
// others use as limit-on-fail. We pass through so the venue decides.
|
|
249
|
+
try {
|
|
250
|
+
// Per CTO 2026-05-31T2240 decision: PER-VENUE endpoints (not a generic
|
|
251
|
+
// /sign/order). Each venue puts policy-relevant fields (symbol/qty/side)
|
|
252
|
+
// in a different location — Binance in query string, OKX in JSON body,
|
|
253
|
+
// Asterdex inside the EIP-712 message. Per-venue handlers enforce policy
|
|
254
|
+
// on the actual fields; a generic endpoint would muddy that.
|
|
255
|
+
const signed = await callGateway(`/sign/${encodeURIComponent(args.venue)}-order`, { method: "POST", body: args, authRequired: true }, cfg);
|
|
256
|
+
const venueResponse = await submitSignedBundle(signed, cfg.fetchImpl, cfg.fetchTimeoutMs);
|
|
257
|
+
return toolJson({ venue: args.venue, response: venueResponse });
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
return toolError(err.message);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Option-A flow for /sign/cancel — same as place_order.
|
|
265
|
+
*
|
|
266
|
+
* Note: Binance + OKX cancel routes require `symbol` on top of `order_id`.
|
|
267
|
+
* The MCP tool exposes optional `symbol` to support these (and ignores it
|
|
268
|
+
* for venues that don't need it).
|
|
269
|
+
*/
|
|
270
|
+
export async function handleCancelOrder(cfg, args) {
|
|
271
|
+
try {
|
|
272
|
+
// Per-venue endpoint per CTO decision (same rationale as place_order).
|
|
273
|
+
const signed = await callGateway(`/sign/${encodeURIComponent(args.venue)}-cancel`, { method: "POST", body: args, authRequired: true }, cfg);
|
|
274
|
+
const venueResponse = await submitSignedBundle(signed, cfg.fetchImpl, cfg.fetchTimeoutMs);
|
|
275
|
+
return toolJson({ venue: args.venue, response: venueResponse });
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
return toolError(err.message);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
//# sourceMappingURL=lib.js.map
|
package/dist/lib.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lib.js","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asterdex account parser — v0 stub.
|
|
3
|
+
*
|
|
4
|
+
* Asterdex is a BSC on-chain perp DEX. Account state lives on-chain (margin
|
|
5
|
+
* vault + position records), so the "account" read isn't a single REST call
|
|
6
|
+
* — it's reading contract state via JSON-RPC or their indexer.
|
|
7
|
+
*
|
|
8
|
+
* For v0, we expect signer's gateway to return a combined payload from the
|
|
9
|
+
* indexer (the signer worker handles this — it's their decision how to
|
|
10
|
+
* shape it). Our parser is tolerant: if the shape matches the expected v0
|
|
11
|
+
* draft, we parse; otherwise return zeros with the raw timestamp.
|
|
12
|
+
*
|
|
13
|
+
* Expected v0 shape (subject to signer worker confirming):
|
|
14
|
+
* {
|
|
15
|
+
* "wallet": "0x...",
|
|
16
|
+
* "equity_usd": "10000.0", // already USD from the indexer
|
|
17
|
+
* "available_margin_usd": "8500.0",
|
|
18
|
+
* "positions": [
|
|
19
|
+
* {
|
|
20
|
+
* "market": "BTC-USD",
|
|
21
|
+
* "size": "0.001", // signed
|
|
22
|
+
* "entry_price": "67000.0",
|
|
23
|
+
* "mark_price": "67200.0",
|
|
24
|
+
* "unrealized_pnl_usd": "0.2"
|
|
25
|
+
* }
|
|
26
|
+
* ],
|
|
27
|
+
* "block_timestamp": 1717180800
|
|
28
|
+
* }
|
|
29
|
+
*
|
|
30
|
+
* If the signer worker chooses a different shape, this parser is the only
|
|
31
|
+
* file that needs to change to absorb it — the public NormalizedAccount
|
|
32
|
+
* contract stays stable.
|
|
33
|
+
*/
|
|
34
|
+
import type { AccountParser } from "./types.js";
|
|
35
|
+
export declare const parseAsterdexAccount: AccountParser;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asterdex account parser — v0 stub.
|
|
3
|
+
*
|
|
4
|
+
* Asterdex is a BSC on-chain perp DEX. Account state lives on-chain (margin
|
|
5
|
+
* vault + position records), so the "account" read isn't a single REST call
|
|
6
|
+
* — it's reading contract state via JSON-RPC or their indexer.
|
|
7
|
+
*
|
|
8
|
+
* For v0, we expect signer's gateway to return a combined payload from the
|
|
9
|
+
* indexer (the signer worker handles this — it's their decision how to
|
|
10
|
+
* shape it). Our parser is tolerant: if the shape matches the expected v0
|
|
11
|
+
* draft, we parse; otherwise return zeros with the raw timestamp.
|
|
12
|
+
*
|
|
13
|
+
* Expected v0 shape (subject to signer worker confirming):
|
|
14
|
+
* {
|
|
15
|
+
* "wallet": "0x...",
|
|
16
|
+
* "equity_usd": "10000.0", // already USD from the indexer
|
|
17
|
+
* "available_margin_usd": "8500.0",
|
|
18
|
+
* "positions": [
|
|
19
|
+
* {
|
|
20
|
+
* "market": "BTC-USD",
|
|
21
|
+
* "size": "0.001", // signed
|
|
22
|
+
* "entry_price": "67000.0",
|
|
23
|
+
* "mark_price": "67200.0",
|
|
24
|
+
* "unrealized_pnl_usd": "0.2"
|
|
25
|
+
* }
|
|
26
|
+
* ],
|
|
27
|
+
* "block_timestamp": 1717180800
|
|
28
|
+
* }
|
|
29
|
+
*
|
|
30
|
+
* If the signer worker chooses a different shape, this parser is the only
|
|
31
|
+
* file that needs to change to absorb it — the public NormalizedAccount
|
|
32
|
+
* contract stays stable.
|
|
33
|
+
*/
|
|
34
|
+
function toNum(v, fallback = 0) {
|
|
35
|
+
if (typeof v === "number")
|
|
36
|
+
return Number.isFinite(v) ? v : fallback;
|
|
37
|
+
if (typeof v === "string" && v.length > 0) {
|
|
38
|
+
const n = parseFloat(v);
|
|
39
|
+
return Number.isFinite(n) ? n : fallback;
|
|
40
|
+
}
|
|
41
|
+
return fallback;
|
|
42
|
+
}
|
|
43
|
+
export const parseAsterdexAccount = (raw) => {
|
|
44
|
+
const obj = raw ?? {};
|
|
45
|
+
const positionsRaw = Array.isArray(obj.positions) ? obj.positions : [];
|
|
46
|
+
const positions = [];
|
|
47
|
+
for (const p of positionsRaw) {
|
|
48
|
+
const r = p ?? {};
|
|
49
|
+
const qty = toNum(r.size);
|
|
50
|
+
if (qty === 0)
|
|
51
|
+
continue;
|
|
52
|
+
const symbol = String(r.market ?? "");
|
|
53
|
+
if (symbol === "")
|
|
54
|
+
continue;
|
|
55
|
+
const out = {
|
|
56
|
+
symbol,
|
|
57
|
+
qty,
|
|
58
|
+
entry_price: toNum(r.entry_price),
|
|
59
|
+
unrealized_pnl: toNum(r.unrealized_pnl_usd),
|
|
60
|
+
};
|
|
61
|
+
if (r.mark_price !== undefined)
|
|
62
|
+
out.mark_price = toNum(r.mark_price);
|
|
63
|
+
positions.push(out);
|
|
64
|
+
}
|
|
65
|
+
let updated_at = new Date(0).toISOString();
|
|
66
|
+
if (typeof obj.block_timestamp === "number" && Number.isFinite(obj.block_timestamp)) {
|
|
67
|
+
// Block timestamps are typically seconds, not ms.
|
|
68
|
+
const ms = obj.block_timestamp > 1e12 ? obj.block_timestamp : obj.block_timestamp * 1000;
|
|
69
|
+
updated_at = new Date(ms).toISOString();
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
venue: "asterdex",
|
|
73
|
+
equity_usd: toNum(obj.equity_usd),
|
|
74
|
+
free_margin_usd: toNum(obj.available_margin_usd),
|
|
75
|
+
positions,
|
|
76
|
+
updated_at,
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
//# sourceMappingURL=asterdex.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asterdex.js","sourceRoot":"","sources":["../../src/parsers/asterdex.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAIH,SAAS,KAAK,CAAC,CAAU,EAAE,QAAQ,GAAG,CAAC;IACrC,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;IACpE,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1C,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;QACxB,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;IAC3C,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,CAAC,MAAM,oBAAoB,GAAkB,CAAC,GAAG,EAAqB,EAAE;IAC5E,MAAM,GAAG,GAAI,GAA+B,IAAI,EAAE,CAAC;IACnD,MAAM,YAAY,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;IAEvE,MAAM,SAAS,GAAyB,EAAE,CAAC;IAC3C,KAAK,MAAM,CAAC,IAAI,YAAY,EAAE,CAAC;QAC7B,MAAM,CAAC,GAAI,CAA6B,IAAI,EAAE,CAAC;QAC/C,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAC1B,IAAI,GAAG,KAAK,CAAC;YAAE,SAAS;QACxB,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;QACtC,IAAI,MAAM,KAAK,EAAE;YAAE,SAAS;QAC5B,MAAM,GAAG,GAAuB;YAC9B,MAAM;YACN,GAAG;YACH,WAAW,EAAE,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC;YACjC,cAAc,EAAE,KAAK,CAAC,CAAC,CAAC,kBAAkB,CAAC;SAC5C,CAAC;QACF,IAAI,CAAC,CAAC,UAAU,KAAK,SAAS;YAAE,GAAG,CAAC,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;QACrE,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACtB,CAAC;IAED,IAAI,UAAU,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;IAC3C,IAAI,OAAO,GAAG,CAAC,eAAe,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,eAAe,CAAC,EAAE,CAAC;QACpF,kDAAkD;QAClD,MAAM,EAAE,GAAG,GAAG,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,GAAG,CAAC,eAAe,GAAG,IAAI,CAAC;QACzF,UAAU,GAAG,IAAI,IAAI,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IAC1C,CAAC;IAED,OAAO;QACL,KAAK,EAAE,UAAU;QACjB,UAAU,EAAE,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC;QACjC,eAAe,EAAE,KAAK,CAAC,GAAG,CAAC,oBAAoB,CAAC;QAChD,SAAS;QACT,UAAU;KACX,CAAC;AACJ,CAAC,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binance USD-M Futures account parser.
|
|
3
|
+
*
|
|
4
|
+
* Endpoint: GET /fapi/v2/account
|
|
5
|
+
* Docs: https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Account-Information-V2
|
|
6
|
+
*
|
|
7
|
+
* Schema (only the fields we use):
|
|
8
|
+
* {
|
|
9
|
+
* "totalMarginBalance": "10000.00", // total equity
|
|
10
|
+
* "totalUnrealizedProfit": "12.34",
|
|
11
|
+
* "availableBalance": "8500.00", // free margin
|
|
12
|
+
* "updateTime": 1717180800000,
|
|
13
|
+
* "positions": [
|
|
14
|
+
* {
|
|
15
|
+
* "symbol": "BTCUSDT",
|
|
16
|
+
* "positionAmt": "0.002", // signed: + long, - short
|
|
17
|
+
* "entryPrice": "67120.5",
|
|
18
|
+
* "unrealizedProfit": "1.23",
|
|
19
|
+
* "markPrice": "67200.0" // not in /v2/account; we leave undefined
|
|
20
|
+
* }
|
|
21
|
+
* ]
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* Notes on quirks:
|
|
25
|
+
* - Binance returns numbers as strings — parseFloat with NaN guards.
|
|
26
|
+
* - positionAmt "0" rows appear for every symbol the account has ever
|
|
27
|
+
* traded; we filter them out so the agent only sees real positions.
|
|
28
|
+
* - markPrice isn't on /v2/account — it's on /v1/premiumIndex per-symbol.
|
|
29
|
+
* Leave undefined; agent uses get_orderbook or similar if needed.
|
|
30
|
+
*/
|
|
31
|
+
import type { AccountParser } from "./types.js";
|
|
32
|
+
export declare const parseBinanceAccount: AccountParser;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binance USD-M Futures account parser.
|
|
3
|
+
*
|
|
4
|
+
* Endpoint: GET /fapi/v2/account
|
|
5
|
+
* Docs: https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Account-Information-V2
|
|
6
|
+
*
|
|
7
|
+
* Schema (only the fields we use):
|
|
8
|
+
* {
|
|
9
|
+
* "totalMarginBalance": "10000.00", // total equity
|
|
10
|
+
* "totalUnrealizedProfit": "12.34",
|
|
11
|
+
* "availableBalance": "8500.00", // free margin
|
|
12
|
+
* "updateTime": 1717180800000,
|
|
13
|
+
* "positions": [
|
|
14
|
+
* {
|
|
15
|
+
* "symbol": "BTCUSDT",
|
|
16
|
+
* "positionAmt": "0.002", // signed: + long, - short
|
|
17
|
+
* "entryPrice": "67120.5",
|
|
18
|
+
* "unrealizedProfit": "1.23",
|
|
19
|
+
* "markPrice": "67200.0" // not in /v2/account; we leave undefined
|
|
20
|
+
* }
|
|
21
|
+
* ]
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* Notes on quirks:
|
|
25
|
+
* - Binance returns numbers as strings — parseFloat with NaN guards.
|
|
26
|
+
* - positionAmt "0" rows appear for every symbol the account has ever
|
|
27
|
+
* traded; we filter them out so the agent only sees real positions.
|
|
28
|
+
* - markPrice isn't on /v2/account — it's on /v1/premiumIndex per-symbol.
|
|
29
|
+
* Leave undefined; agent uses get_orderbook or similar if needed.
|
|
30
|
+
*/
|
|
31
|
+
function toNum(v, fallback = 0) {
|
|
32
|
+
if (typeof v === "number")
|
|
33
|
+
return Number.isFinite(v) ? v : fallback;
|
|
34
|
+
if (typeof v === "string") {
|
|
35
|
+
const n = parseFloat(v);
|
|
36
|
+
return Number.isFinite(n) ? n : fallback;
|
|
37
|
+
}
|
|
38
|
+
return fallback;
|
|
39
|
+
}
|
|
40
|
+
function toIso(updateTime) {
|
|
41
|
+
if (typeof updateTime === "number" && Number.isFinite(updateTime)) {
|
|
42
|
+
return new Date(updateTime).toISOString();
|
|
43
|
+
}
|
|
44
|
+
return new Date(0).toISOString(); // epoch sentinel — caller can detect
|
|
45
|
+
}
|
|
46
|
+
export const parseBinanceAccount = (raw) => {
|
|
47
|
+
const obj = raw ?? {};
|
|
48
|
+
const positionsRaw = Array.isArray(obj.positions) ? obj.positions : [];
|
|
49
|
+
const positions = positionsRaw
|
|
50
|
+
.map((p) => {
|
|
51
|
+
const r = p ?? {};
|
|
52
|
+
const qty = toNum(r.positionAmt);
|
|
53
|
+
if (qty === 0)
|
|
54
|
+
return null;
|
|
55
|
+
return {
|
|
56
|
+
symbol: String(r.symbol ?? ""),
|
|
57
|
+
qty,
|
|
58
|
+
entry_price: toNum(r.entryPrice),
|
|
59
|
+
unrealized_pnl: toNum(r.unrealizedProfit),
|
|
60
|
+
// markPrice intentionally undefined — not on /v2/account
|
|
61
|
+
};
|
|
62
|
+
})
|
|
63
|
+
.filter((p) => p !== null && p.symbol !== "");
|
|
64
|
+
return {
|
|
65
|
+
venue: "binance",
|
|
66
|
+
equity_usd: toNum(obj.totalMarginBalance),
|
|
67
|
+
free_margin_usd: toNum(obj.availableBalance),
|
|
68
|
+
positions,
|
|
69
|
+
updated_at: toIso(obj.updateTime),
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
//# sourceMappingURL=binance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"binance.js","sourceRoot":"","sources":["../../src/parsers/binance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAIH,SAAS,KAAK,CAAC,CAAU,EAAE,QAAQ,GAAG,CAAC;IACrC,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;IACpE,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;QAC1B,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;QACxB,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;IAC3C,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,KAAK,CAAC,UAAmB;IAChC,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;QAClE,OAAO,IAAI,IAAI,CAAC,UAAU,CAAC,CAAC,WAAW,EAAE,CAAC;IAC5C,CAAC;IACD,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,qCAAqC;AACzE,CAAC;AAED,MAAM,CAAC,MAAM,mBAAmB,GAAkB,CAAC,GAAG,EAAqB,EAAE;IAC3E,MAAM,GAAG,GAAI,GAA+B,IAAI,EAAE,CAAC;IACnD,MAAM,YAAY,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;IAEvE,MAAM,SAAS,GAAyB,YAAY;SACjD,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QACT,MAAM,CAAC,GAAI,CAA6B,IAAI,EAAE,CAAC;QAC/C,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;QACjC,IAAI,GAAG,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAC3B,OAAO;YACL,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC;YAC9B,GAAG;YACH,WAAW,EAAE,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC;YAChC,cAAc,EAAE,KAAK,CAAC,CAAC,CAAC,gBAAgB,CAAC;YACzC,yDAAyD;SAC7B,CAAC;IACjC,CAAC,CAAC;SACD,MAAM,CAAC,CAAC,CAAC,EAA2B,EAAE,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,MAAM,KAAK,EAAE,CAAC,CAAC;IAEzE,OAAO;QACL,KAAK,EAAE,SAAS;QAChB,UAAU,EAAE,KAAK,CAAC,GAAG,CAAC,kBAAkB,CAAC;QACzC,eAAe,EAAE,KAAK,CAAC,GAAG,CAAC,gBAAgB,CAAC;QAC5C,SAAS;QACT,UAAU,EAAE,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC;KAClC,CAAC;AACJ,CAAC,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Venue parser dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* Under the Option-A architecture (per signer 2026-05-31T2150 schema doc),
|
|
5
|
+
* signer-mcp:
|
|
6
|
+
* 1. calls signer gateway /account/<venue> → gets a signed read request
|
|
7
|
+
* 2. fetches the venue URL with the signed headers
|
|
8
|
+
* 3. dispatches the raw response to the venue-specific parser here
|
|
9
|
+
* 4. returns a NormalizedAccount to the agent
|
|
10
|
+
*
|
|
11
|
+
* Adding a venue = adding a parser file + entry below. Pure TS, no enclave
|
|
12
|
+
* involvement.
|
|
13
|
+
*/
|
|
14
|
+
import type { AccountParser } from "./types.js";
|
|
15
|
+
export declare function getAccountParser(venue: string): AccountParser | undefined;
|
|
16
|
+
export type { NormalizedAccount, NormalizedPosition, SignedRequest } from "./types.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Venue parser dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* Under the Option-A architecture (per signer 2026-05-31T2150 schema doc),
|
|
5
|
+
* signer-mcp:
|
|
6
|
+
* 1. calls signer gateway /account/<venue> → gets a signed read request
|
|
7
|
+
* 2. fetches the venue URL with the signed headers
|
|
8
|
+
* 3. dispatches the raw response to the venue-specific parser here
|
|
9
|
+
* 4. returns a NormalizedAccount to the agent
|
|
10
|
+
*
|
|
11
|
+
* Adding a venue = adding a parser file + entry below. Pure TS, no enclave
|
|
12
|
+
* involvement.
|
|
13
|
+
*/
|
|
14
|
+
import { parseAsterdexAccount } from "./asterdex.js";
|
|
15
|
+
import { parseBinanceAccount } from "./binance.js";
|
|
16
|
+
import { parseOkxAccount } from "./okx.js";
|
|
17
|
+
const PARSERS = {
|
|
18
|
+
binance: parseBinanceAccount,
|
|
19
|
+
okx: parseOkxAccount,
|
|
20
|
+
asterdex: parseAsterdexAccount,
|
|
21
|
+
};
|
|
22
|
+
export function getAccountParser(venue) {
|
|
23
|
+
return PARSERS[venue];
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/parsers/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAG3C,MAAM,OAAO,GAAkC;IAC7C,OAAO,EAAE,mBAAmB;IAC5B,GAAG,EAAE,eAAe;IACpB,QAAQ,EAAE,oBAAoB;CAC/B,CAAC;AAEF,MAAM,UAAU,gBAAgB,CAAC,KAAa;IAC5C,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC;AACxB,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OKX v5 account parser.
|
|
3
|
+
*
|
|
4
|
+
* OKX splits balance and positions across TWO endpoints:
|
|
5
|
+
* GET /api/v5/account/balance → equity + free margin
|
|
6
|
+
* GET /api/v5/account/positions → open positions
|
|
7
|
+
*
|
|
8
|
+
* For the Option-A architecture this means signer's `/account/<venue>` for
|
|
9
|
+
* OKX may need to return TWO signed requests (or one composite). For v0 we
|
|
10
|
+
* accept a combined raw payload:
|
|
11
|
+
*
|
|
12
|
+
* {
|
|
13
|
+
* "balance": { ...OKX /balance response... },
|
|
14
|
+
* "positions": { ...OKX /positions response... }
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* If only `balance` is present (positions response missing), we still emit
|
|
18
|
+
* a valid NormalizedAccount with empty positions — the agent can decide.
|
|
19
|
+
*
|
|
20
|
+
* OKX response wrapper: { "code": "0", "msg": "", "data": [ {...} ] }
|
|
21
|
+
* Single-element data arrays are typical for these endpoints.
|
|
22
|
+
*
|
|
23
|
+
* Docs:
|
|
24
|
+
* https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-balance
|
|
25
|
+
* https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-positions
|
|
26
|
+
*
|
|
27
|
+
* Quirks:
|
|
28
|
+
* - OKX returns numbers as strings (like Binance).
|
|
29
|
+
* - Empty equity field returns "" not null.
|
|
30
|
+
* - Positions array can include zeroed-out rows after closes; filter them.
|
|
31
|
+
* - `instId` is OKX's symbol field (e.g. "BTC-USDT-SWAP").
|
|
32
|
+
*/
|
|
33
|
+
import type { AccountParser } from "./types.js";
|
|
34
|
+
export declare const parseOkxAccount: AccountParser;
|