Skip to content

Commit 2676d89

Browse files
committed
feat: export Metaculus exchange, sidecar, compliance, and Python token auth
- Export MetaculusExchange from core index and pmxt default map; add apiToken to ExchangeCredentials for Authorization: Token header. - Register metaculus on HTTP sidecar; honor apiToken for per-request instances. - Complete Metaculus has map; document token requirement; fix event category when Metaculus returns string or object categories. - Regenerate COMPLIANCE.md and Python _exchanges (Metaculus + api_token on Exchange client); compliance initExchange reads METACULUS_API_TOKEN. - README: add Metaculus to supported exchanges. - Probable: cast wallet for createClobClient when duplicate viem installs break WalletClient assignability (tsc). Made-with: Cursor
1 parent a810fc3 commit 2676d89

File tree

15 files changed

+123
-30
lines changed

15 files changed

+123
-30
lines changed

core/COMPLIANCE.md

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,28 @@ This document details the feature support and compliance status for each exchang
88

99
## Functions Status
1010

11-
| Category | Function | Polymarket | Kalshi | Limitless | Probable | Baozi | Myriad | Opinion |
12-
| :--- | :--- | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
13-
| **Market Data** | `fetchMarkets` | Y | Y | Y | Y | Y | Y | Y |
14-
| | `fetchEvents` | Y | Y | Y | Y | Y | Y | Y |
15-
| | `fetchMarket` | Y | Y | Y | Y | Y | Y | Y |
16-
| | `fetchEvent` | Y | Y | Y | Y | Y | Y | Y |
17-
| **Public Data** | `fetchOHLCV` | Y | Y | Y | Y | Y | Y | Y |
18-
| | `fetchOrderBook` | Y | Y | Y | Y | Y | Y | Y |
19-
| | `fetchTrades` | Y | Y | Y | Y | Y | Y | - |
20-
| **Private Data** | `fetchBalance` | Y | Y | Y | Y | Y | Y | - |
21-
| | `fetchPositions` | Y | Y | Y | Y | Y | Y | Y |
22-
| | `fetchMyTrades` | Y | Y | Y | Y | - | Y | Y |
23-
| **Trading** | `createOrder` | Y | Y | Y | Y | Y | Y | Y |
24-
| | `cancelOrder` | Y | Y | Y | Y | Y | - | Y |
25-
| | `fetchOrder` | Y | Y | Y | Y | Y | - | Y |
26-
| | `fetchOpenOrders` | Y | Y | Y | Y | Y | Y | Y |
27-
| | `fetchClosedOrders` | - | Y | Y | - | - | - | Y |
28-
| | `fetchAllOrders` | - | Y | Y | - | - | - | Y |
29-
| **Calculations** | `getExecutionPrice` | Y | Y | Y | Y | Y | Y | Y |
30-
| | `getExecutionPriceDetailed` | Y | Y | Y | Y | Y | Y | Y |
31-
| **Real-time** | `watchOrderBook` | Y | Y | Y | Y | Y | Y | Y |
32-
| | `watchTrades` | Y | Y | Y | - | - | Y | Y |
11+
| Category | Function | Polymarket | Kalshi | Limitless | Probable | Baozi | Myriad | Opinion | Metaculus |
12+
| :--- | :--- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
13+
| **Market Data** | `fetchMarkets` | Y | Y | Y | Y | Y | Y | Y | Y |
14+
| | `fetchEvents` | Y | Y | Y | Y | Y | Y | Y | Y |
15+
| | `fetchMarket` | Y | Y | Y | Y | Y | Y | Y | Y |
16+
| | `fetchEvent` | Y | Y | Y | Y | Y | Y | Y | Y |
17+
| **Public Data** | `fetchOHLCV` | Y | Y | Y | Y | Y | Y | Y | - |
18+
| | `fetchOrderBook` | Y | Y | Y | Y | Y | Y | Y | - |
19+
| | `fetchTrades` | Y | Y | Y | Y | Y | Y | - | - |
20+
| **Private Data** | `fetchBalance` | Y | Y | Y | Y | Y | Y | - | - |
21+
| | `fetchPositions` | Y | Y | Y | Y | Y | Y | Y | - |
22+
| | `fetchMyTrades` | Y | Y | Y | Y | - | Y | Y | - |
23+
| **Trading** | `createOrder` | Y | Y | Y | Y | Y | Y | Y | - |
24+
| | `cancelOrder` | Y | Y | Y | Y | Y | - | Y | - |
25+
| | `fetchOrder` | Y | Y | Y | Y | Y | - | Y | - |
26+
| | `fetchOpenOrders` | Y | Y | Y | Y | Y | Y | Y | - |
27+
| | `fetchClosedOrders` | - | Y | Y | - | - | - | Y | - |
28+
| | `fetchAllOrders` | - | Y | Y | - | - | - | Y | - |
29+
| **Calculations** | `getExecutionPrice` | Y | Y | Y | Y | Y | Y | Y | - |
30+
| | `getExecutionPriceDetailed` | Y | Y | Y | Y | Y | Y | Y | - |
31+
| **Real-time** | `watchOrderBook` | Y | Y | Y | Y | Y | Y | Y | - |
32+
| | `watchTrades` | Y | Y | Y | - | - | Y | Y | - |
3333

3434
## Legend
3535
- **Y** - Supported
@@ -50,4 +50,6 @@ LIMITLESS_PRIVATE_KEY=0x...
5050
# Myriad
5151
MYRIAD_API_KEY=...
5252
MYRIAD_WALLET_ADDRESS=0x...
53+
# Metaculus (required for API access — unauthenticated requests return 403)
54+
METACULUS_API_TOKEN=...
5355
```

core/scripts/generate-compliance.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const METHOD_CATEGORIES = [
2323
];
2424

2525
// Exchange display order (skip kalshi-demo since it inherits Kalshi fully)
26-
const EXCHANGE_ORDER = ['polymarket', 'kalshi', 'limitless', 'probable', 'baozi', 'myriad', 'opinion'];
26+
const EXCHANGE_ORDER = ['polymarket', 'kalshi', 'limitless', 'probable', 'baozi', 'myriad', 'opinion', 'metaculus'];
2727

2828
function toDisplayName(slug) {
2929
return slug.split('-').map(p => p.charAt(0).toUpperCase() + p.slice(1)).join('');
@@ -187,6 +187,8 @@ LIMITLESS_PRIVATE_KEY=0x...
187187
# Myriad
188188
MYRIAD_API_KEY=...
189189
MYRIAD_WALLET_ADDRESS=0x...
190+
# Metaculus (required for API access — unauthenticated requests return 403)
191+
METACULUS_API_TOKEN=...
190192
\`\`\`
191193
`;
192194

core/scripts/generate-python-exchanges.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ function build(name, block) {
8484
name,
8585
creds: {
8686
apiKey: /credentials\?\.apiKey/.test(block),
87+
apiToken: /credentials\?\.apiToken/.test(block),
8788
apiSecret: /credentials\?\.apiSecret/.test(block),
8889
passphrase: /credentials\?\.passphrase/.test(block),
8990
privateKey: /credentials\?\.privateKey/.test(block),
@@ -110,6 +111,10 @@ function generateClass(exchange) {
110111
constructorParams.push('api_key: Optional[str] = None');
111112
superArgs.push('api_key=api_key');
112113
}
114+
if (creds.apiToken) {
115+
constructorParams.push('api_token: Optional[str] = None');
116+
superArgs.push('api_token=api_token');
117+
}
113118
if (creds.apiSecret) {
114119
constructorParams.push('api_secret: Optional[str] = None');
115120
extraAttrs.push('self.api_secret = api_secret');
@@ -142,6 +147,7 @@ function generateClass(exchange) {
142147

143148
const docLines = [];
144149
if (creds.apiKey) docLines.push(' api_key: API key for authentication (optional)');
150+
if (creds.apiToken) docLines.push(' api_token: API token for authentication (optional; required for Metaculus API access)');
145151
if (creds.apiSecret) docLines.push(' api_secret: API secret for authentication (optional)');
146152
if (creds.passphrase) docLines.push(' passphrase: Passphrase for authentication (optional)');
147153
if (creds.privateKey) {

core/src/BaseExchange.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,8 @@ export interface ExchangeCredentials {
200200
apiKey?: string;
201201
apiSecret?: string;
202202
passphrase?: string;
203+
/** Metaculus: `Authorization: Token <apiToken>` for higher rate limits */
204+
apiToken?: string;
203205

204206
// Blockchain-based authentication (Polymarket)
205207
privateKey?: string; // Required for Polymarket L1 auth

core/src/exchanges/metaculus/fetchEvents.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,12 @@ function postToEvent(post: any): UnifiedEvent | null {
7272
volume: 0,
7373
url: `https://www.metaculus.com/questions/${id}/`,
7474
image: post.projects?.default_project?.header_image ?? undefined,
75-
category: (post?.projects?.category?.[0]?.name) ?? undefined,
75+
category:
76+
post?.projects?.category?.[0] != null
77+
? typeof post.projects.category[0] === "string"
78+
? post.projects.category[0]
79+
: post.projects.category[0]?.name
80+
: undefined,
7681
tags: market.tags ?? [],
7782
};
7883
}

core/src/exchanges/metaculus/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ import { BASE_URL } from "./utils";
1212
import { fetchMarkets } from "./fetchMarkets";
1313
import { fetchEvents } from "./fetchEvents";
1414

15+
/**
16+
* Read-only Metaculus integration (community forecasts, no CLOB).
17+
* The live API returns 403 for unauthenticated requests; pass `{ apiToken }`
18+
* from your Metaculus account (header `Authorization: Token …`).
19+
*/
1520
export class MetaculusExchange extends PredictionMarketExchange {
1621
override readonly has = {
1722
fetchMarkets: true as const,
@@ -26,11 +31,15 @@ export class MetaculusExchange extends PredictionMarketExchange {
2631
fetchOpenOrders: false as const,
2732
fetchPositions: false as const,
2833
fetchBalance: false as const,
34+
watchAddress: false as const,
35+
unwatchAddress: false as const,
2936
watchOrderBook: false as const,
3037
watchTrades: false as const,
3138
fetchMyTrades: false as const,
3239
fetchClosedOrders: false as const,
3340
fetchAllOrders: false as const,
41+
buildOrder: false as const,
42+
submitOrder: false as const,
3443
};
3544

3645
private readonly apiToken?: string;

core/src/exchanges/probable/auth.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,18 +52,22 @@ export class ProbableAuth {
5252
passphrase: this.credentials.passphrase!,
5353
};
5454

55+
// @prob/clob may resolve a different viem copy than this package; types then
56+
// disagree on WalletClient. Runtime shape is identical.
57+
const walletForClob = wallet as any;
58+
5559
if (chainId === 56) {
5660
this.clobClient = createClobClient({
5761
chainId: 56,
58-
wallet,
62+
wallet: walletForClob,
5963
credential,
6064
});
6165
} else {
6266
const baseUrl = process.env.PROBABLE_BASE_URL || 'https://api.probable.markets/public/api/v1';
6367
this.clobClient = createClobClient({
6468
chainId,
6569
baseUrl,
66-
wallet,
70+
wallet: walletForClob,
6771
credential,
6872
});
6973
}

core/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export * from './exchanges/probable';
1111
export * from './exchanges/baozi';
1212
export * from './exchanges/myriad';
1313
export * from './exchanges/opinion';
14+
export * from './exchanges/metaculus';
1415
export * from './server/app';
1516
export * from './server/utils/port-manager';
1617
export * from './server/utils/lock-file';
@@ -23,6 +24,7 @@ import { ProbableExchange } from './exchanges/probable';
2324
import { BaoziExchange } from './exchanges/baozi';
2425
import { MyriadExchange } from './exchanges/myriad';
2526
import { OpinionExchange } from './exchanges/opinion';
27+
import { MetaculusExchange } from './exchanges/metaculus';
2628

2729
const pmxt = {
2830
Polymarket: PolymarketExchange,
@@ -33,6 +35,7 @@ const pmxt = {
3335
Baozi: BaoziExchange,
3436
Myriad: MyriadExchange,
3537
Opinion: OpinionExchange,
38+
Metaculus: MetaculusExchange,
3639
};
3740

3841
export const Polymarket = PolymarketExchange;
@@ -43,5 +46,6 @@ export const Probable = ProbableExchange;
4346
export const Baozi = BaoziExchange;
4447
export const Myriad = MyriadExchange;
4548
export const Opinion = OpinionExchange;
49+
export const Metaculus = MetaculusExchange;
4650

4751
export default pmxt;

core/src/server/app.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ProbableExchange } from "../exchanges/probable";
88
import { BaoziExchange } from "../exchanges/baozi";
99
import { MyriadExchange } from "../exchanges/myriad";
1010
import { OpinionExchange } from "../exchanges/opinion";
11+
import { MetaculusExchange } from "../exchanges/metaculus";
1112
import { ExchangeCredentials } from "../BaseExchange";
1213
import { BaseError } from "../errors";
1314

@@ -21,6 +22,7 @@ const defaultExchanges: Record<string, any> = {
2122
baozi: null,
2223
myriad: null,
2324
opinion: null,
25+
metaculus: null,
2426
};
2527

2628
export async function startServer(port: number, accessToken: string) {
@@ -64,7 +66,12 @@ export async function startServer(port: number, accessToken: string) {
6466
// If credentials are provided, create a new instance for this request
6567
// Otherwise, use the singleton instance
6668
let exchange: any;
67-
if (credentials && (credentials.privateKey || credentials.apiKey)) {
69+
if (
70+
credentials &&
71+
(credentials.privateKey ||
72+
credentials.apiKey ||
73+
credentials.apiToken)
74+
) {
6875
exchange = createExchange(exchangeName, credentials);
6976
} else {
7077
if (!defaultExchanges[exchangeName]) {
@@ -216,6 +223,11 @@ function createExchange(name: string, credentials?: ExchangeCredentials) {
216223
credentials?.privateKey || process.env.OPINION_PRIVATE_KEY,
217224
funderAddress: credentials?.funderAddress,
218225
});
226+
case "metaculus":
227+
return new MetaculusExchange({
228+
apiToken:
229+
credentials?.apiToken || process.env.METACULUS_API_TOKEN,
230+
});
219231
default:
220232
throw new Error(`Unknown exchange: ${name}`);
221233
}

core/test/compliance/shared.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,5 +462,10 @@ export function initExchange(name: string, cls: any) {
462462
walletAddress: process.env.OPINION_WALLET_ADDRESS?.trim(),
463463
});
464464
}
465+
if (name === "MetaculusExchange") {
466+
return new cls({
467+
apiToken: process.env.METACULUS_API_TOKEN?.trim(),
468+
});
469+
}
465470
return new cls();
466471
}

0 commit comments

Comments
 (0)