mirror of
https://github.com/affaan-m/ECC.git
synced 2026-08-17 21:15:40 +02:00
Harden Itô market intelligence skill (#2711)
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
---
|
||||
name: ito-market-intelligence
|
||||
description: Research prediction-market events, venues, underliers, liquidity, and news context for Itô basket workflows. Use for read-only market intelligence, API-gated Itô exploration, and source-grounded prediction-market briefings without investment advice or live trading.
|
||||
metadata:
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Itô Market Intelligence
|
||||
@@ -10,8 +8,9 @@ metadata:
|
||||
Use this skill when a user wants prediction-market context, event discovery,
|
||||
venue comparison, basket theme exploration, or an Itô API-backed market brief.
|
||||
|
||||
This is a public teaser skill. It can work with public sources by default. Any
|
||||
Itô-backed data call requires explicit API access through `ITO_API_KEY`.
|
||||
Use public sources by default. Any Itô-backed data call requires the user to
|
||||
explicitly request Itô data and requires a scoped `ITO_API_KEY`. Never print,
|
||||
persist, or ask the user to paste a key into chat.
|
||||
|
||||
## Guardrails
|
||||
|
||||
@@ -21,13 +20,27 @@ Itô-backed data call requires explicit API access through `ITO_API_KEY`.
|
||||
- Treat Polymarket, Kalshi, Itô, X, Exa, GitHub, and web data as source inputs,
|
||||
not as truth by themselves.
|
||||
- Separate facts, market-implied signals, and your interpretation.
|
||||
- Never claim a price, volume, liquidity value, timestamp, venue rule, or news
|
||||
event that is absent from a cited response or source.
|
||||
- Treat every remote response as a snapshot. Show its retrieval time, source
|
||||
URL, and source-provided update time when available. Call data stale or
|
||||
unknown rather than silently treating it as current.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Clarify the market theme, venue, geography, and time horizon.
|
||||
2. Gather public market data from venue docs/APIs or source-grounded research.
|
||||
3. If `ITO_API_KEY` is present and the user explicitly asks for Itô data, call
|
||||
only read endpoints and state that access is gated.
|
||||
Cite the exact source URL next to each material claim and distinguish the
|
||||
publication/update time from the retrieval time.
|
||||
3. If the user explicitly asks for Itô data, run the bundled read-only client:
|
||||
|
||||
```bash
|
||||
node scripts/ito-market-intelligence.js --json search-markets --platform all --limit 25
|
||||
```
|
||||
|
||||
The client reads `ITO_API_KEY` from the environment, sends it only to the
|
||||
configured Itô HTTPS origin, never logs it, and permits only documented GET
|
||||
endpoints. Do not run it merely because a key exists.
|
||||
4. Normalize event, underlier, liquidity, fee, resolution, and data-latency
|
||||
differences across venues.
|
||||
5. Produce a decision brief:
|
||||
@@ -37,6 +50,23 @@ Itô-backed data call requires explicit API access through `ITO_API_KEY`.
|
||||
- relevant news/source context
|
||||
- open questions before any user action
|
||||
|
||||
## Authentication and recovery
|
||||
|
||||
- Market-data API keys are separate from the Itô compute CLI's device login.
|
||||
Do not run `ito login`, `ecc ito login`, or open a browser for this skill:
|
||||
those credentials are not a documented substitute for a `baskets:read` or
|
||||
`markets:read` API key. Return control to the originating agent after stating
|
||||
the missing scope and operator-driven access requirement.
|
||||
- On `AUTH_MISSING`, request a scoped key through the user's established Itô
|
||||
access channel without collecting it in chat. On `AUTH_REJECTED`, say the key
|
||||
may be expired, revoked, or missing the required read scope.
|
||||
- On `RATE_LIMITED`, respect `retry_after_seconds`; do not loop automatically.
|
||||
On `TIMEOUT` or `UPSTREAM_ERROR`, preserve prior cited facts, label the live
|
||||
snapshot unavailable, and offer a bounded retry. Never replace failed live
|
||||
data with invented values.
|
||||
- `ITO_MARKET_API_URL` may override the API origin for deterministic local
|
||||
tests. In normal use keep the default `https://itomarkets.com/api/v1`.
|
||||
|
||||
## Useful Skill Chains
|
||||
|
||||
- Use `deep-research` or `exa-search` for source discovery.
|
||||
@@ -47,7 +77,9 @@ Itô-backed data call requires explicit API access through `ITO_API_KEY`.
|
||||
|
||||
## Output Contract
|
||||
|
||||
Default to a compact brief with source links and a clear caveat:
|
||||
Default to a compact brief containing `retrieved_at`, source links,
|
||||
source-provided timestamps, freshness caveats, facts, market-implied signals,
|
||||
interpretation, and actionable open questions. End with:
|
||||
|
||||
```text
|
||||
This is market intelligence, not investment or trading advice.
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Itô Market Intelligence"
|
||||
short_description: "Source-grounded prediction-market intelligence"
|
||||
default_prompt: "Use $ito-market-intelligence to create a current, source-grounded prediction-market brief with provenance and freshness caveats."
|
||||
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const DEFAULT_BASE_URL = 'https://itomarkets.com/api/v1';
|
||||
const DEFAULT_TIMEOUT_MS = 10_000;
|
||||
|
||||
function fail(code, message, details = {}, exitCode = 1) {
|
||||
const error = new Error(message);
|
||||
Object.assign(error, { code, details, exitCode });
|
||||
throw error;
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = argv.slice(2);
|
||||
const options = { json: false, timeoutMs: DEFAULT_TIMEOUT_MS, params: {} };
|
||||
while (args[0]?.startsWith('--')) {
|
||||
const flag = args.shift();
|
||||
if (flag === '--json') options.json = true;
|
||||
else if (flag === '--timeout-ms') options.timeoutMs = Number(args.shift());
|
||||
else fail('USAGE', `Unknown global option: ${flag}`, {}, 2);
|
||||
}
|
||||
options.command = args.shift();
|
||||
while (args.length) {
|
||||
const flag = args.shift();
|
||||
if (!flag?.startsWith('--') || !args.length) fail('USAGE', `Invalid option: ${flag || '(missing)'}`, {}, 2);
|
||||
options.params[flag.slice(2)] = args.shift();
|
||||
}
|
||||
if (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 100 || options.timeoutMs > 60_000) {
|
||||
fail('USAGE', '--timeout-ms must be an integer from 100 to 60000', {}, 2);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function commandPath(command, params) {
|
||||
const enc = encodeURIComponent;
|
||||
if (command === 'list-baskets') return ['/baskets', new Set(['page', 'per-page'])];
|
||||
if (command === 'search-markets') return ['/markets/search', new Set(['platform', 'category', 'expiration', 'limit'])];
|
||||
if (command === 'get-market' && params['market-id']) return [`/markets/${enc(params['market-id'])}`, new Set(['platform'])];
|
||||
if (command === 'market-history' && params['market-id']) return [`/markets/${enc(params['market-id'])}/history`, new Set(['platform', 'days'])];
|
||||
fail('USAGE', 'Use list-baskets, search-markets, get-market --market-id ID, or market-history --market-id ID', {}, 2);
|
||||
}
|
||||
|
||||
function safeBaseUrl(raw) {
|
||||
let url;
|
||||
try { url = new URL(raw); } catch { fail('CONFIG', 'ITO_MARKET_API_URL must be an absolute URL'); }
|
||||
const local = ['localhost', '127.0.0.1', '::1'].includes(url.hostname);
|
||||
if (url.protocol !== 'https:' && !(url.protocol === 'http:' && local)) {
|
||||
fail('CONFIG', 'ITO_MARKET_API_URL must use HTTPS (HTTP is allowed only for loopback tests)');
|
||||
}
|
||||
url.pathname = url.pathname.replace(/\/$/, '');
|
||||
url.search = '';
|
||||
url.hash = '';
|
||||
return url;
|
||||
}
|
||||
|
||||
async function run(options, environment = process.env, fetchImpl = fetch) {
|
||||
const apiKey = environment.ITO_API_KEY?.trim();
|
||||
if (!apiKey) fail('AUTH_MISSING', 'No Itô market API credential is configured. Set ITO_API_KEY outside chat.');
|
||||
const base = safeBaseUrl(environment.ITO_MARKET_API_URL || DEFAULT_BASE_URL);
|
||||
const [pathname, allowed] = commandPath(options.command, options.params);
|
||||
const url = new URL(`${base.pathname}${pathname}`, base);
|
||||
for (const [key, value] of Object.entries(options.params)) {
|
||||
if (key === 'market-id') continue;
|
||||
if (!allowed.has(key)) fail('USAGE', `Option --${key} is not valid for ${options.command}`, {}, 2);
|
||||
url.searchParams.set(key === 'per-page' ? 'per_page' : key, value);
|
||||
}
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), options.timeoutMs);
|
||||
const retrievedAt = new Date().toISOString();
|
||||
let response;
|
||||
try {
|
||||
response = await fetchImpl(url, {
|
||||
method: 'GET',
|
||||
headers: { Authorization: `Bearer ${apiKey}`, Accept: 'application/json' },
|
||||
signal: controller.signal,
|
||||
redirect: 'error',
|
||||
});
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') fail('TIMEOUT', `Itô market API did not respond within ${options.timeoutMs}ms`);
|
||||
fail('UPSTREAM_ERROR', 'Itô market API request failed');
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
let body;
|
||||
try { body = await response.json(); } catch { fail('INVALID_RESPONSE', 'Itô market API returned non-JSON content'); }
|
||||
if (response.status === 401 || response.status === 403) fail('AUTH_REJECTED', 'Itô rejected the credential or required read scope');
|
||||
if (response.status === 429) {
|
||||
const retry = Number(response.headers.get('retry-after'));
|
||||
fail('RATE_LIMITED', 'Itô market API rate limit reached', Number.isFinite(retry) ? { retry_after_seconds: retry } : {});
|
||||
}
|
||||
if (!response.ok) fail('UPSTREAM_ERROR', `Itô market API returned HTTP ${response.status}`, { status: response.status });
|
||||
const rateLimit = {};
|
||||
for (const [field, header] of [['limit', 'x-ratelimit-limit'], ['remaining', 'x-ratelimit-remaining'], ['reset_epoch', 'x-ratelimit-reset']]) {
|
||||
const value = Number(response.headers.get(header));
|
||||
if (Number.isFinite(value)) rateLimit[field] = value;
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
command: options.command,
|
||||
retrieved_at: retrievedAt,
|
||||
source: { provider: 'Itô Markets', url: url.toString(), http_status: response.status },
|
||||
freshness: { source_updated_at: body?.meta?.updated_at || body?.data?.updated_at || null, caveat: 'Snapshot at retrieval time; verify source timestamps before acting.' },
|
||||
rate_limit: Object.keys(rateLimit).length ? rateLimit : null,
|
||||
data: body?.data ?? body,
|
||||
meta: body?.meta ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function print(result, json) {
|
||||
if (json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
||||
else process.stdout.write(`${result.command}: ${JSON.stringify(result.data)}\nSource: ${result.source.url}\nRetrieved: ${result.retrieved_at}\n`);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
let options = { json: process.argv.includes('--json') };
|
||||
Promise.resolve().then(() => { options = parseArgs(process.argv); return run(options); })
|
||||
.then(result => print(result, options.json))
|
||||
.catch(error => {
|
||||
const payload = { ok: false, error: { code: error.code || 'INTERNAL', message: error.message, ...(error.details && Object.keys(error.details).length ? { details: error.details } : {}) } };
|
||||
process.stderr.write(`${options.json ? JSON.stringify(payload, null, 2) : `${payload.error.code}: ${payload.error.message}`}\n`);
|
||||
process.exitCode = error.exitCode || 1;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { parseArgs, run, safeBaseUrl };
|
||||
@@ -0,0 +1,84 @@
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
const { parseArgs, run } = require('../../skills/ito-market-intelligence/scripts/ito-market-intelligence');
|
||||
|
||||
const ROOT = path.join(__dirname, '..', '..');
|
||||
const SKILL = path.join(ROOT, 'skills', 'ito-market-intelligence');
|
||||
const CLIENT = path.join(SKILL, 'scripts', 'ito-market-intelligence.js');
|
||||
|
||||
function invoke(args, env = {}) {
|
||||
return spawnSync(process.execPath, [CLIENT, '--json', ...args], {
|
||||
encoding: 'utf8', env: { PATH: process.env.PATH, ...env }, timeout: 5000,
|
||||
});
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const skill = fs.readFileSync(path.join(SKILL, 'SKILL.md'), 'utf8');
|
||||
assert.match(skill, /^---\nname: ito-market-intelligence\ndescription: [^\n]+\n---/);
|
||||
assert.doesNotMatch(skill.split('---')[1], /\nmetadata:/);
|
||||
for (const trigger of ['event discovery', 'venue comparison', 'basket theme', 'market brief']) assert.ok(skill.includes(trigger));
|
||||
for (const contract of ['retrieved_at', 'source-provided timestamps', 'AUTH_REJECTED', 'RATE_LIMITED', 'TIMEOUT']) assert.ok(skill.includes(contract));
|
||||
const agentMetadata = fs.readFileSync(path.join(SKILL, 'agents', 'openai.yaml'), 'utf8');
|
||||
assert.match(agentMetadata, /display_name: "Itô Market Intelligence"/);
|
||||
assert.match(agentMetadata, /default_prompt: "Use \$ito-market-intelligence /);
|
||||
|
||||
let result = invoke(['search-markets']);
|
||||
assert.strictEqual(result.status, 1);
|
||||
assert.strictEqual(JSON.parse(result.stderr).error.code, 'AUTH_MISSING');
|
||||
|
||||
result = invoke(['search-markets'], { ITO_API_KEY: 'secret', ITO_MARKET_API_URL: 'http://example.com/api/v1' });
|
||||
assert.strictEqual(JSON.parse(result.stderr).error.code, 'CONFIG');
|
||||
assert.ok(!result.stderr.includes('secret'));
|
||||
|
||||
const fetchSuccess = async (url, request) => {
|
||||
assert.strictEqual(request.method, 'GET');
|
||||
assert.strictEqual(request.headers.Authorization, 'Bearer test-key');
|
||||
assert.match(url.toString(), /\/markets\/search\?platform=all&limit=1$/);
|
||||
return new Response(JSON.stringify({ data: [{ market_id: 'm1', title: 'Example' }], meta: { updated_at: '2026-08-07T12:00:00Z' } }), { status: 200, headers: { 'x-ratelimit-limit': '120', 'x-ratelimit-remaining': '119', 'x-ratelimit-reset': '1786128733' } });
|
||||
};
|
||||
const payload = await run(parseArgs(['node', CLIENT, 'search-markets', '--platform', 'all', '--limit', '1']), { ITO_API_KEY: 'test-key' }, fetchSuccess);
|
||||
assert.strictEqual(payload.ok, true);
|
||||
assert.strictEqual(payload.source.provider, 'Itô Markets');
|
||||
assert.strictEqual(payload.freshness.source_updated_at, '2026-08-07T12:00:00Z');
|
||||
assert.deepStrictEqual(payload.rate_limit, { limit: 120, remaining: 119, reset_epoch: 1786128733 });
|
||||
assert.deepStrictEqual(payload.data, [{ market_id: 'm1', title: 'Example' }]);
|
||||
assert.ok(!JSON.stringify(payload).includes('test-key'));
|
||||
|
||||
const fetchPage = async url => {
|
||||
assert.match(url.toString(), /\/baskets\?page=2&per_page=5$/);
|
||||
return new Response(JSON.stringify({ data: [], meta: { page: 2, per_page: 5 } }), { status: 200 });
|
||||
};
|
||||
const pagePayload = await run(parseArgs(['node', CLIENT, 'list-baskets', '--page', '2', '--per-page', '5']), { ITO_API_KEY: 'test-key' }, fetchPage);
|
||||
assert.strictEqual(pagePayload.meta.per_page, 5);
|
||||
|
||||
await assert.rejects(
|
||||
run(parseArgs(['node', CLIENT, 'list-baskets']), { ITO_API_KEY: 'revoked' }, async () => new Response('{}', { status: 401 })),
|
||||
error => error.code === 'AUTH_REJECTED' && !error.message.includes('revoked')
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
run(parseArgs(['node', CLIENT, 'list-baskets']), { ITO_API_KEY: 'key' }, async () => new Response('{}', { status: 429, headers: { 'retry-after': '7' } })),
|
||||
error => error.code === 'RATE_LIMITED' && error.details.retry_after_seconds === 7
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
run(parseArgs(['node', CLIENT, '--timeout-ms', '100', 'list-baskets']), { ITO_API_KEY: 'key' }, async (_url, request) => new Promise((_resolve, reject) => {
|
||||
request.signal.addEventListener('abort', () => reject(Object.assign(new Error('aborted'), { name: 'AbortError' })));
|
||||
})),
|
||||
error => error.code === 'TIMEOUT' && !error.message.includes('key')
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
run(parseArgs(['node', CLIENT, 'list-baskets']), { ITO_API_KEY: 'key' }, async () => new Response('<html>bad gateway</html>', { status: 502 })),
|
||||
error => error.code === 'INVALID_RESPONSE' && !error.message.includes('bad gateway')
|
||||
);
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(ROOT, 'manifests', 'install-modules.json')));
|
||||
assert.ok(manifest.modules.some(module => module.paths?.includes('skills/ito-market-intelligence')));
|
||||
const packed = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'))).files;
|
||||
assert.ok(packed.includes('skills/ito-market-intelligence/'));
|
||||
fs.accessSync(CLIENT, fs.constants.R_OK);
|
||||
console.log('PASS ito-market-intelligence skill contract');
|
||||
})().catch(error => { console.error(error); process.exitCode = 1; });
|
||||
Reference in New Issue
Block a user