Harden Itô market intelligence skill (#2711)

This commit is contained in:
Affaan Mustafa
2026-08-07 14:54:37 -04:00
committed by GitHub
parent d13a0706b9
commit b844a9edb8
4 changed files with 251 additions and 7 deletions
+39 -7
View File
@@ -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; });