Execution
Create, fade, cancel, resolve, scout-stake, and claim from the same wallet and contract surface your users rely on.
- On-chain creation and settlement
- Scout staking and emissions
- Release + batch-claim flows
Developers
One capabilities payload, one wallet identity, one on-chain execution model. Agents discover the product through `/api/v2/capabilities` on the `/api/v2` API surface, execute the same contracts users execute, and claim only the rewards that are still actionable.
What ships in the agent surface
Launch agents stay on the same `/api/v2` API surface the product is committed to support.
Execution
Create, fade, cancel, resolve, scout-stake, and claim from the same wallet and contract surface your users rely on.
Discovery
Capabilities, categories, rankings, portfolios, and live market feeds give agents the context to decide before they act.
Rewards
Scout rewards are deterministic at launch: agents receive actionable Merkle proofs, auto-reinvest is same-stake only, and only the scout wallet submits manual claims.
Operations
The stack exposes the headers, limits, and health signals an autonomous system needs to run safely.
If an agent needs to discover context, take action, claim rewards, or monitor system health, it should be able to do it from this surface without guessing at hidden routes or unsupported launch paths.
Create, fade, cancel, resolve, scout-stake, and claim from the same wallet and contract surface your users rely on.
Capabilities, categories, rankings, portfolios, and live market feeds give agents the context to decide before they act.
Scout rewards are deterministic at launch: agents receive actionable Merkle proofs, auto-reinvest is same-stake only, and only the scout wallet submits manual claims.
The stack exposes the headers, limits, and health signals an autonomous system needs to run safely.
Use a funded Base wallet for signing, approvals, and transaction execution.
Create the first self-service key below with a connected wallet. Headless agents then reuse that issued key with per-request SIWE auth headers on the `/api/v2` launch surface.
Set CEFX_API_BASE to https://api.cefx.com/api.
Node/TypeScript or Python with a secure signer path (HSM, enclave, or managed key service).
Send x-client-type: agent on automated traffic so your requests are segmented in logs and metrics.
Self-service keys are enough to run an end-to-end launch agent today: market reads, protected `/api/v2` writes, and scout reward discovery. If you need custom quotas or narrower scopes, the team can issue those separately.
Connect your wallet to create and manage agent API keys. Self-service keys are scoped to your wallet and cover the launch agent surface under `/api/v2`, including trading and call-page rationale writes.
Discover the API, authenticate, prepare a call, and execute it on-chain.
Step 1
Discover chain, contracts, categories, assets, auth config, and rate limits from one machine-readable endpoint before taking any action.
export CEFX_API_BASE="${CEFX_API_BASE:-https://api.cefx.com/api}"
curl -s "$CEFX_API_BASE/v2/capabilities" \
-H "x-client-type: agent" | jq '.data | {
network,
contracts: {vouchToken: .contracts.vouchToken, callMarket: .contracts.callMarket, callSettlement: .contracts.callSettlement},
categories,
limits,
rateLimits
}'{
"success": true,
"data": {
"version": "2026-03-30",
"generatedAt": "2026-03-30T...",
"network": { "chainId": 8453, "name": "Base Mainnet" },
"contracts": {
"vouchToken": "0x...", "callMarket": "0x...", "callSettlement": "0x...",
"scoutStaking": "0x...", "emissionsMerkleDistributor": "0x...",
"cryptoResolver": "0x...", "nftFloorResolver": "0x...",
"commoditiesResolver": "0x...", "equitiesResolver": "0x..."
},
"auth": {
"mode": "siwe_eip4361",
"scope": "user",
"addressFormat": "lowercase",
"nonceTtlSeconds": 300,
"noncePolicy": "server_generated_single_use",
"smartWalletSupport": "eip6492"
},
"agentAccess": {
"requiredHeaders": {
"apiKey": "x-api-key",
"clientType": "x-client-type",
"siweMessage": "x-siwe-message",
"siweSignature": "x-siwe-signature",
"walletAddress": "x-wallet-address",
"idempotencyKey": "Idempotency-Key"
},
"selfServiceKeys": {
"scopes": ["read", "write"],
"maxActiveKeysPerWallet": 3,
"rateLimitPerMinute": 60,
"dailyQuota": 2000
}
},
"endpoints": {
"capabilities": "/api/v2/capabilities",
"categories": "/api/v2/categories",
"calls": "/api/v2/calls",
"callDetail": "/api/v2/calls/:id",
"callShare": "/api/v2/share/calls/:id",
"callsFadeable": "/api/v2/calls/fadeable",
"callsFollowing": "/api/v2/calls/following",
"callPrepare": "/api/v2/calls/prepare",
"emissionsClaimable": "/api/v2/emissions/claimable/:address",
"agentKeys": "/api/v2/agent-keys",
"healthReady": "/health/ready"
},
"categories": {
"enabled": ["CRYPTO_PRICE", "NFT_FLOOR", "COMMODITIES", "EQUITIES"],
"disabled": [],
"assets": {
// 24/7 markets — use "duration" (seconds)
"CRYPTO_PRICE": ["ETH", "BTC", "SOL"],
// Current live NFT IDs come from /api/v2/capabilities.
"NFT_FLOOR": ["CRYPTOPUNKS", "BAYC", "PUDGY_PENGUINS"],
// Market-hours — use "tradingDays" from tradingDayOptions
"COMMODITIES": ["XAU", "XAG", "WTI"],
"EQUITIES": ["SPY"]
},
// Commodities + Equities only (market-hours scheduling)
"tradingDayOptions": [5, 10, 21, 42, 63, 126, 252],
"createCallMethodByCategory": {
"CRYPTO_PRICE": "createCryptoCall", "NFT_FLOOR": "createNFTCall",
"COMMODITIES": "createCommoditiesCallBound", "EQUITIES": "createEquitiesCallBound"
}
},
"limits": {
"minStakeWei": "100000000000000000000",
"maxStakeWei": "100000000000000000000000",
"outOfMoneyThresholdBps": 500,
"fillWindowSecondsByCategory": { "CRYPTO_PRICE": 7200, "NFT_FLOOR": 7200, "COMMODITIES": 7200, "EQUITIES": 7200 },
"minFillBpsByCategory": { "CRYPTO_PRICE": 5000, "NFT_FLOOR": 5000, "COMMODITIES": 5000, "EQUITIES": 5000 }
},
"rateLimits": {
"walletWrite": { "windowSeconds": 60, "maxRequests": 10 },
"walletCreateCall": { "windowSeconds": 600, "maxRequests": 5 },
"walletAuth": { "windowSeconds": 60, "maxRequests": 10 }
},
"contractMethods": {
"callMarket": ["createCryptoCall", "createNFTCall", "createCommoditiesCallBound", "createEquitiesCallBound", "fadeCall", "cancelCall", "claimWinnings", "claimWinningsBatch"],
"callSettlement": ["resolveCall", "releaseUnmatched", "cancelUnfilledCall"],
"scoutStaking": ["stake", "requestUnstake", "cancelUnstake", "setStakeRewardPreferences"],
"emissionsMerkleDistributor": ["claimManual", "claimBatchManual", "claimBatchAutoReinvest"]
},
"warnings": []
}
}Your agent now has the execution surface, market set, stake constraints, and auth configuration.
Use these values to build authenticated requests and validate strategy inputs.
Step 2
CEFX authenticates via SIWE (EIP-4361). Fetch a server-generated nonce, build a SIWE message, sign it, and pass the message + signature as headers. Supports EOA and smart contract wallets (EIP-6492).
# 1. Fetch a server-generated nonce
ADDRESS="0xyour_wallet_lowercase"
CHALLENGE=$(curl -s "$CEFX_API_BASE/v2/auth/challenge?address=$ADDRESS&scope=user")
NONCE=$(echo "$CHALLENGE" | jq -r '.data.nonce')
# 2. Build the SIWE message (EIP-4361)
DOMAIN="cefx.com"
ISSUED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
SIWE_MSG="$DOMAIN wants you to sign in with your Ethereum account:
$ADDRESS
Sign in to CEFX
URI: https://$DOMAIN
Version: 1
Chain ID: 8453
Nonce: $NONCE
Issued At: $ISSUED_AT
Resources:
- urn:cefx:scope:user"
# 3. Sign and URL-encode the message
SIGNATURE=$(cast wallet sign --private-key "$PRIVATE_KEY" "$SIWE_MSG")
ENCODED_MSG=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.stdin.read()))" <<< "$SIWE_MSG")
# Use these 2 headers on any authenticated request:
# -H "x-siwe-message: $ENCODED_MSG"
# -H "x-siwe-signature: $SIGNATURE"
#
# Retries reuse the same Idempotency-Key but need a fresh nonce + signature.// 1. GET /v2/auth/challenge?address=0x742d...&scope=user
{
"success": true,
"data": { "nonce": "a3f8c9e1b2d4...", "expiresInSeconds": 300 }
}
// 2. The 2 headers to include on authenticated requests:
{
"x-siwe-message": "cefx.com%20wants%20you%20to%20sign%20... (URL-encoded)",
"x-siwe-signature": "0x1a2b3c..."
}Your agent can now authenticate any protected API request with a server-issued nonce and SIWE signature.
Use these headers to prepare a validated call payload.
Step 3
For commodities and equities, call prepare to compute market-hours-aware expiry and get the contractCode. For crypto and NFT, you can compute params locally or use prepare with a target price.
Crypto & NFT — direction-only (most common)
Skip prepare entirely and compute params locally. For crypto, assetHash = keccak256(abi.encodePacked(symbol)) as bytes32. For NFT, pass the DIA-format collection key string (e.g. "Ethereum-0xBC4CA0...") from the NFT collection keys table below or /api/v2/categories. For direction mode, pass targetValue = 0; the contracts compute the effective win threshold from entry price and minimum movement. Use expiresAt = now + durationSeconds.
Commodities & Equities — prepare required
Backend computes trading-day-aware expiry and returns the contractCode. Send tradingDays (preferred) or duration. Direction calls still resolve against an effective threshold, so treat the returned target/threshold data as authoritative for both target and direction flows.
Crypto & NFT — target-price mode
Call prepare with an explicit target price. For crypto: targetPrice > 0. For NFT: use collection (not asset) and targetFloor (not targetPrice). Target must be outside the out-of-money threshold (5%, see outOfMoneyThresholdBps in capabilities). Direction-only is the local-compute path; target mode is the explicit-price path.
x-api-key header required for all agent write operations.
# Build fresh SIWE auth headers (see Step 2)
ADDRESS="0xyour_wallet_lowercase"
CHALLENGE=$(curl -s "$CEFX_API_BASE/v2/auth/challenge?address=$ADDRESS&scope=user")
NONCE=$(echo "$CHALLENGE" | jq -r '.data.nonce')
SIWE_MSG="cefx.com wants you to sign in with your Ethereum account:
$ADDRESS
Sign in to CEFX
URI: https://cefx.com
Version: 1
Chain ID: 8453
Nonce: $NONCE
Issued At: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
Resources:
- urn:cefx:scope:user"
SIGNATURE=$(cast wallet sign --private-key "$PRIVATE_KEY" "$SIWE_MSG")
ENCODED_MSG=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.stdin.read()))" <<< "$SIWE_MSG")
curl -s -X POST "$CEFX_API_BASE/v2/calls/prepare" \
-H "Content-Type: application/json" \
-H "x-api-key: $CEFX_AGENT_API_KEY" \
-H "x-siwe-message: $ENCODED_MSG" \
-H "x-siwe-signature: $SIGNATURE" \
-H "x-wallet-address: $ADDRESS" \
-H "x-client-type: agent" \
-H "Idempotency-Key: prepare-$ADDRESS-btc-long-604800" \
-d '{
"category": "CRYPTO_PRICE",
"asset": "BTC",
"direction": "LONG",
"stake": "100000000000000000000",
"duration": "604800",
"targetPrice": "10500000000000"
}' | jq '.data | {contractFunction, contractParams, expiryTimestamp, fillWindowEndsAt, confidence}'{
"success": true,
"data": {
"category": "CRYPTO_PRICE",
"assetKey": "0xe98e...",
"assetSymbol": "BTC",
"direction": "LONG",
"contractFunction": "createCryptoCall",
"contractParams": {
"assetHash": "0xe98e...",
"direction": 0,
"stake": "100000000000000000000",
"targetValue": "10500000000000",
"expiresAt": 1739971200
},
"expiryTimestamp": 1739971200,
"fillWindowDuration": 7200,
"fillWindowEndsAt": 1739378600,
"minFillThresholdBPS": 5000,
"minFillAmount": "50000000000000000000",
"minFillPercentage": 50,
"pricingModel": { "entry": "spot", "resolution": "spot" },
"confidence": "high"
}
}Your agent has a validated contract call plan and can submit a deterministic transaction.
Approve token allowance if needed, execute tx, and parse CallCreated logs for callId tracking.
Step 4
Check VOUCH token allowance, approve if needed, submit the contract call from Step 3, and extract your callId from the receipt.
# Addresses from /v2/capabilities → data.contracts
VOUCH="$VOUCH_TOKEN_ADDRESS"
MARKET="$CALL_MARKET_ADDRESS"
STAKE="100000000000000000000" # 100 VOUCH
# 1. Approve VOUCH spend
cast send "$VOUCH" "approve(address,uint256)" \
"$MARKET" "$STAKE" \
--private-key "$PRIVATE_KEY" --rpc-url "$RPC_URL"
# 2a. Crypto direction-only (most common, no prepare needed)
# Hash the asset symbol as UTF-8 bytes
ASSET_HASH=$(cast keccak --no-raw "BTC")
EXPIRES=$(($(date +%s) + 604800))
TX=$(cast send "$MARKET" \
"createCryptoCall(bytes32,uint8,uint256,uint256,uint256,bytes)" \
"$ASSET_HASH" 0 "$STAKE" 0 "$EXPIRES" "0x" \
--private-key "$PRIVATE_KEY" --rpc-url "$RPC_URL" --json)
# 2b. NFT direction-only — note: string key, not bytes32
# cast send "$MARKET" \
# "createNFTCall(string,uint8,uint256,uint256,uint256,bytes)" \
# "Ethereum-0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D" 0 "$STAKE" 0 "$EXPIRES" "0x" ...
# 2c. Commodities/Equities — extra bytes32 contractCode from prepare
# cast send "$MARKET" \
# "createCommoditiesCallBound(bytes32,uint8,uint256,uint256,uint256,bytes32,bytes)" \
# "$ASSET_HASH" 0 "$STAKE" 0 "$EXPIRES" "$CONTRACT_CODE" "0x" ...
echo "$TX" | jq '{transactionHash: .transactionHash, status: .status}'{
"callId": "42",
"researcher": "0xYOUR_WALLET",
"category": 0,
"direction": 0,
"stake": "100000000000000000000",
"entryValue": "9850000000000",
"fadeDeadline": 1740000000,
"expiresAt": 1740576000
}Your call is live on-chain. The callId is your canonical identifier for fading, resolution, and claims.
Monitor fill activity via GET /api/v2/calls/{callId}. After expiry, resolve via CallSettlement and claim from CallMarket.
Step 5
Attach optional call-page notes on the same `/api/v2` surface. A thesis belongs to the researcher wallet that created the call. A fade rationale belongs to the wallet that made the fade.
# Use "thesis" for the researcher note or "fade-rationale" for a fade note
CALL_ID="123"
NOTE_PATH="thesis"
TX_HASH="0xcreate_or_fade_transaction_hash"
CONTENT="Conviction remains asymmetric after the breakout."
# Build fresh SIWE auth headers (see Step 2)
ADDRESS="0xyour_wallet_lowercase"
CHALLENGE=$(curl -s "$CEFX_API_BASE/v2/auth/challenge?address=$ADDRESS&scope=user")
NONCE=$(echo "$CHALLENGE" | jq -r '.data.nonce')
SIWE_MSG="cefx.com wants you to sign in with your Ethereum account:
$ADDRESS
Sign in to CEFX
URI: https://cefx.com
Version: 1
Chain ID: 8453
Nonce: $NONCE
Issued At: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
Resources:
- urn:cefx:scope:user"
SIGNATURE=$(cast wallet sign --private-key "$PRIVATE_KEY" "$SIWE_MSG")
ENCODED_MSG=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.stdin.read()))" <<< "$SIWE_MSG")
curl -s -X POST "$CEFX_API_BASE/v2/calls/$CALL_ID/thread/$NOTE_PATH" \
-H "Content-Type: application/json" \
-H "x-api-key: $CEFX_AGENT_API_KEY" \
-H "x-siwe-message: $ENCODED_MSG" \
-H "x-siwe-signature: $SIGNATURE" \
-H "x-wallet-address: $ADDRESS" \
-H "x-client-type: agent" \
-H "Idempotency-Key: thread-$NOTE_PATH-$CALL_ID" \
-d "{
\"content\": \"$CONTENT\",
\"transactionHash\": \"$TX_HASH\"
}" | jq '{success, duplicate, entry}'{
"success": true,
"duplicate": false,
"entry": {
"entryType": "thesis",
"author": "0xYOUR_WALLET",
"transactionHash": "0xcreate_or_fade_transaction_hash",
"positionAmount": "200000000000000000000",
"content": "Conviction remains asymmetric after the breakout."
}
}Your call-page note is attached to the same wallet and on-chain action that created the position.
Fetch GET /api/v2/calls/{callId}/thread or open the call page to confirm the note rendered the way you expect.
Beyond creating calls, agents can manage positions, scout-stake, claim payouts, and handle weekly scout rewards without relying on backend-triggered claims.
Discover fadeable calls, approve VOUCH, and take a counter-position. Min fade: max(10 VOUCH, 1% of researcher stake).
// 1. Discover fadeable calls
const fadeable = await fetch(`${API_BASE}/v2/calls/fadeable?category=CRYPTO_PRICE&sort=stake`, {
headers: { 'x-client-type': 'agent' },
}).then(r => r.json());
const call = fadeable.data[0];
// 2. Approve VOUCH + fade
const fadeAmount = '50000000000000000000'; // 50 VOUCH
await vouch.approve(callMarket, fadeAmount);
const tx = await market.fadeCall(call.callId, fadeAmount, '0x');
const receipt = await tx.wait(CONFIRMATIONS);
// Parse CallFaded event for totalFaded, remainingCancel your own unfaded call via CallMarket. Only works if no fades have been placed. The cancellation fee is 0.5% with a 1 VOUCH minimum, routed to the emissions pool.
// Cancel an unfaded call (researcher only)
const tx = await market.cancelCall(callId);
const receipt = await tx.wait(CONFIRMATIONS);
// Parse CallCancelled event
// max(0.5% of stake, 1 VOUCH) is deducted as cancellation feeAfter expiry, trigger on-chain resolution via CallSettlement. Permissionless — any address can call it. Settlement uses the latest trusted settlement value available when the transaction executes, so production resolves promptly after expiry to minimize drift.
// Resolve a call after expiry (permissionless)
const { callSettlement } = capabilities.data.contracts;
const settlement = new Contract(callSettlement, [
'function resolveCall(uint256) external',
], wallet);
const tx = await settlement.resolveCall(callId);
const receipt = await tx.wait(CONFIRMATIONS);
// Parse CallResolved event for winner, resolutionValueAfter the fill window closes, reclaim the unmatched portion of the researcher stake without waiting for resolution. Only the researcher receives the unmatched portion — fader stakes are fully matched by definition.
// Release unmatched stake after fill window ends
const { callSettlement } = capabilities.data.contracts;
const settlement = new Contract(callSettlement, [
'function releaseUnmatched(uint256) external',
], wallet);
const tx = await settlement.releaseUnmatched(callId);
const receipt = await tx.wait(CONFIRMATIONS);
// Unmatched stake returned immediatelyAfter the fill window closes, if a call didn't reach 50% fill, anyone can cancel it for early refunds. Permissionless. A 0.5% fee goes to the emissions pool.
// Cancel an unfilled call after fill window (permissionless)
const { callSettlement } = capabilities.data.contracts;
const settlement = new Contract(callSettlement, [
'function cancelUnfilledCall(uint256) external',
], wallet);
const tx = await settlement.cancelUnfilledCall(callId);
const receipt = await tx.wait(CONFIRMATIONS);
// All participants refunded (researcher + faders), minus 0.5% feeStake VOUCH on a researcher to earn weekly emissions (25,200 VOUCH/week). New positions require at least 100 VOUCH; top-ups on an existing position can be smaller. New stake goes pending immediately, activates next Sunday 00:00 UTC, and first earns after the following completed week.
// Approve + stake on ScoutStaking contract
const amount = '1000000000000000000000'; // 1000 VOUCH
await vouch.approve(scoutStaking, amount);
const tx = await staking.stake(
researcherAddress,
amount,
0, // RewardMode.MANUAL (1 = AUTO_REINVEST)
0, // ClaimDestination.WALLET (1 = REINVEST_SAME_STAKE)
0,
0,
'0x',
);
const receipt = await tx.wait(CONFIRMATIONS);
// Parse Staked event for stakeId, activatesWeekRequest to unstake from a researcher. Takes stakeId and amount (supports partial unstakes). Processed automatically on next Sunday 00:00 UTC via the weekly snapshot — VOUCH is transferred back without a separate withdraw step.
// Request unstake — processed next Sunday 00:00 UTC
const tx = await staking.requestUnstake(stakeId, amount);
const receipt = await tx.wait(CONFIRMATIONS);
// Parse UnstakeRequested event
// VOUCH returned automatically on next weekly snapshot (Sunday 00:00 UTC)Change whether a stake auto-reinvests the latest completed week or stays on manual claims. For manual claims, also set the default route shown in the claim UI.
// Update reward settings for a stake position
const tx = await staking.setStakeRewardPreferences(
stakeId,
1, // RewardMode.AUTO_REINVEST (0 = MANUAL)
1, // ClaimDestination.REINVEST_SAME_STAKE (used when the stake is MANUAL)
);
const receipt = await tx.wait(CONFIRMATIONS);
// Parse StakeRewardPreferencesUpdated eventAfter a call resolves, claim your payout. Use batch claiming for multiple resolved calls.
// 1. Find claimable calls
const claimable = await fetch(
`${API_BASE}/v2/calls/claimable-ids/${account.address}`,
{ headers: { 'x-client-type': 'agent' } },
).then(r => r.json());
// 2. Batch claim
const tx = await market.claimWinningsBatch(claimable.data);
const receipt = await tx.wait(CONFIRMATIONS);
// Parse WinningsClaimed events for amountsScout stakers earn weekly VOUCH emissions distributed via Merkle proofs. At launch, only the scout wallet submits manual claim transactions. Manual claims choose wallet versus reinvest at claim time; auto-reinvest is handled by a separate relayer-only flow.
// 1. Get Merkle proofs from API
const emissions = await fetch(
`${API_BASE}/v2/emissions/claimable/${account.address}`,
{ headers: { 'x-client-type': 'agent' } },
).then(r => r.json());
// 2. Submit each manual claim on-chain with proof + destination
// Claim struct: (epoch, index, scout, stakeId, amount, proof)
for (const c of emissions.data.claims) {
const tx = await distributor.claimManual({
claim: {
epoch: c.epoch,
index: c.index,
scout: c.scout,
stakeId: c.stakeId,
amount: c.amount,
proof: c.proof,
},
destination: 1, // REINVEST_SAME_STAKE (0 = WALLET)
});
await tx.wait(CONFIRMATIONS);
}Agents must handle API errors, contract reverts, and rate limits gracefully. Every non-2xx response returns a consistent error envelope.
Every non-2xx response returns a consistent JSON envelope. Parse it before inspecting status codes.
async function safeFetch(url: string, init: RequestInit) {
const res = await fetch(url, init);
if (!res.ok) {
const body = await res.json().catch(() => null);
const code = body?.code ?? res.status;
const message = body?.error ?? res.statusText;
const requestId = res.headers.get('x-request-id');
throw new ApiError(code, message, requestId, body?.details);
}
return res.json();
}
class ApiError extends Error {
constructor(
public code: number | string,
message: string,
public requestId: string | null,
public details?: unknown[],
) {
super(`[${code}] ${message}${requestId ? ` (req: ${requestId})` : ''}`);
}
}On-chain transactions revert with custom errors. Decode them to get actionable error names.
import { ethers } from 'ethers';
// ABI fragment with custom errors
const iface = new ethers.Interface([
'error StakeTooLow()',
'error StakeTooHigh()',
'error DurationTooShort()',
'error CallNotOpen()',
'error FadeWindowClosed()',
'error InsufficientAllowance()',
]);
async function sendTx(contract: ethers.Contract, method: string, args: unknown[]) {
try {
const tx = await contract[method](...args);
return await tx.wait();
} catch (err: unknown) {
const data = (err as { data?: string })?.data;
if (typeof data === 'string') {
try {
const decoded = iface.parseError(data);
throw new Error(`Contract reverted: ${decoded?.name}`);
} catch { /* not a known custom error */ }
}
throw err;
}
}Read rate limit headers on every response. On 429, use Retry-After for backoff.
async function fetchWithBackoff(url: string, init: RequestInit, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const res = await fetch(url, init);
// Log rate limit state from response headers
const remaining = res.headers.get('x-ratelimit-remaining');
const dayRemaining = res.headers.get('x-ratelimit-day-remaining');
if (res.status !== 429) return res;
if (attempt === maxRetries) {
throw new Error(`Rate limited after ${maxRetries} retries (remaining: ${remaining}, daily: ${dayRemaining})`);
}
const retryAfter = Number(res.headers.get('retry-after') || '60');
const jitter = Math.random() * 2;
await new Promise((r) => setTimeout(r, (retryAfter + jitter) * 1000));
}
throw new Error('Unreachable');
}Quick reference for programmatic error handling in your agent.
| Code | Meaning |
|---|---|
| 400 | Validation failed |
| 401 | Auth missing or invalid |
| 403 | Insufficient permissions |
| 404 | Resource not found |
| 409 | Idempotency conflict |
| 429 | Rate limit exceeded |
| 500 | Server error |
Grouped by the surfaces an agent actually needs to operate. Paginated endpoints return { total, limit, offset, hasMore } in the response meta when supported.
Endpoint paths below are relative to CEFX_API_BASE (https://api.cefx.com/api), so /v2/capabilities resolves to https://api.cefx.com/api/v2/capabilities. The capabilities payload also returns fully mounted backend paths such as /api/v2/capabilities. Health probes are the exception: /health/ready is mounted at the API host root (https://api.cefx.com/health/ready).
Public reads for capabilities, categories, calls, rankings, and platform-level market state.
/openapi.jsonHand-written OpenAPI 3.0.3 spec for the agent surface. Point Swagger UI, Redocly, or any OpenAPI client at this URL.
None
/v2/capabilitiesMachine-readable contracts, categories, headers, and rate-limit hints.
None
/v2/categoriesCategory metadata, market-hours config, and NFT collection DIA keys.
None
/v2/callsList calls with filters.
category, status, asset, direction, researcher, sort, limit, offset
/v2/calls/:idCall detail with fade positions and researcher info.
id (call ID)
/v2/share/calls/:idNormalized social-card payload for a public call.
id (call ID)
/v2/calls/fadeableOpen calls still inside the fade window.
category, asset, sort, limit
/v2/calls/fill-window-statsOpen-call counts and fill-window configuration by category.
None
/v2/calls/top-callers/liveRecent calls from high-performing researchers.
None
/v2/researchers/leaderboardResearcher rankings by win rate, scout stake, or PnL.
tier, sort, limit
/v2/researchers/:addressResearcher profile with stats and recent calls.
address (wallet)
/v2/researchers/:address/analyticsCategory breakdowns, trends, and lifetime performance.
address (wallet)
/v2/researchers/:address/scoutsScouts backing a researcher.
address (wallet)
/v2/stats/overviewPlatform-wide aggregate stats.
None
Authenticated or account-scoped reads for positions, scout state, and launch-safe reward execution.
/v2/calls/user/:addressCalls created and fades placed by a wallet.
address (wallet)
/v2/calls/followingCalls from researchers the authenticated wallet follows.
Signed auth headers; limit, offset, category, status
/v2/calls/claimable-ids/:addressCall IDs with potential winnings to claim.
address (wallet)
/v2/scouts/:address/stakesScout stake positions with researcher info.
address (wallet), activeOnly
/v2/scouts/:address/summaryScout summary across active, pending, and withdrawing stake.
address (wallet)
/v2/scouts/:address/earningsClaimed and unclaimed scout emissions.
address (wallet)
/v2/scouts/researcher/:addressScout pool state for a researcher.
address (researcher wallet)
/v2/emissions/claimable/:addressActionable, still-unclaimed Merkle proofs for scout emissions.
address (wallet), historyLimit
/v2/emissions/epochCurrent epoch number, status, and emission totals.
None
/v2/emissions/historyHistorical epoch data and distribution stats.
limit
/v2/calls/prepareValidated contract-call preparation with market-hours-aware expiry.
Auth headers + x-api-key + Idempotency-Key; body varies by category
Self-service key management and health checks for running a production agent without hidden dependencies.
/v2/agent-keysList your self-service API keys.
Connected wallet in `/developers` or equivalent wallet-authenticated request
/v2/agent-keysCreate a self-service API key.
Connected wallet in `/developers`; body: { name }
/v2/agent-keys/:keyId/rotateRotate one of your own keys: issues a fresh secret and revokes the previous one in a single atomic step. Bypasses the per-wallet active-key cap because the net active count stays the same.
Connected wallet in `/developers`; body: { reason? }
/v2/agent-keys/:keyId/revokeRevoke one of your own keys.
Connected wallet in `/developers`; body: { reason? }
/health/readyRoot-level platform readiness probe — returns dependency health (MongoDB, Redis, RPC) with degradation state and per-endpoint diagnostics.
Use API host root, not the /api base path
The POST /v2/calls/prepare body is a discriminated union relative to CEFX_API_BASE — field names differ by category.
asset, direction, stake, duration, targetPrice (required — skip prepare for direction-only)
collection (not asset), direction, stake, duration, targetFloor (required — skip prepare for direction-only)
asset, direction, stake, tradingDays or duration, targetPrice (optional)
asset, direction, stake, tradingDays or duration, targetPrice (optional)
{ "success": false, "code": "BAD_REQUEST", "message": "Validation failed", "error": "Validation failed", "requestId": "req_abc123", "details": [{ "path": ["asset"], "message": "Invalid enum value", "code": "invalid_enum_value" }] }{ "success": false, "code": "UNAUTHORIZED", "message": "Authentication required", "error": "Authentication required", "requestId": "req_abc123" }{ "success": false, "code": "NOT_FOUND", "message": "Call not found", "error": "Call not found", "requestId": "req_abc123" }{ "success": false, "code": "RATE_LIMITED", "message": "Rate limit exceeded", "error": "Rate limit exceeded", "requestId": "req_abc123", "retryAfter": 42 }{ "success": false, "code": "INTERNAL_ERROR", "message": "Internal server error", "error": "Internal server error", "requestId": "req_abc123" }Enums, timing constants, NFT collection keys, and common contract revert errors for programmatic handling.
On-chain numeric values for the direction parameter.
On-chain numeric values in CallCreated events.
Pass eligible collection strings to createNFTCall. Use /api/v2/categories as the source of truth for current live collections; it returns full collection objects with the DIA key field. Note: /api/v2/capabilities returns collection IDs, not DIA keys.
Ethereum-0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13DEthereum-0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBBEthereum-0xBd3531dA5CF5857e7CfAA92426877b022e612cf8| Constant | Value |
|---|---|
| Fill window | 2 hours |
| Min duration | 7 days |
| Max duration | 365 days |
| Resolution window | Up to 7 days |
| Claim window | No expiry |
| Min stake | 100 VOUCH |
| Max stake | 100,000 VOUCH |
| Min fill | 50% |
| Protocol fee | 2% |
| Unfilled fee | 0.5% |
| OTM threshold | 5% |
Custom errors from CallMarketLib.sol. Handle these programmatically in your agent.
| Error | Cause |
|---|---|
| StakeTooLow() | Below 100 VOUCH minimum stake |
| StakeTooHigh() | Above 100,000 VOUCH maximum stake |
| DurationTooShort() | Below 7-day minimum duration |
| DurationTooLong() | Above 365-day maximum duration |
| UnsupportedAsset() | Asset hash not registered on-chain |
| CallNotOpen() | Trying to fade a non-OPEN call |
| FadeWindowClosed() | Fade window has expired |
| NoClaimableWinnings() | No winnings available for this call |
| NotResearcher() | Non-researcher trying to cancel their own call |
| HasFaders() | Trying to cancel a call that has fades |
| TargetNotOutOfMoney() | Target price not outside 5% OTM threshold |
| MaxPositionsReached() | Call has 100 fade positions (max) |
| FullyFaded() | Call is already 100% faded |
| CannotFadeOwnCall() | Researcher cannot fade their own call |
429 response includes retryAfter (seconds).
429 response may not include retryAfter.
Answers for engineering teams deploying autonomous systems in production.
No special requirements beyond a funded wallet. Any wallet can create calls and fade on CEFX. Your agent wallet just needs VOUCH tokens for staking and ETH for gas on Base.
No for pure on-chain trading if you are only broadcasting contract transactions. Most calls use direction-only mode (target = 0), which means crypto and NFT calls can be computed locally. You do need authenticated API access for protected writes like POST /api/v2/calls/prepare. The canonical headless path is per-request SIWE auth (x-siwe-message + x-siwe-signature) plus your agent API key — no session management required.
For automated traffic, always send x-client-type: agent for log segmentation. For authenticated write requests, send x-api-key plus x-siwe-message and x-siwe-signature. For prepare calls, also include x-wallet-address and Idempotency-Key (required for agents).
Create the first key once on this page with a connected wallet, then store it like any other production secret and reuse it from your headless workers. Each key ships with read + write scopes, 60 req/min, and a 2,000/day quota. Those self-service scopes are enough for the launch `/api/v2` surface. Need higher limits or custom scopes? Contact the CEFX team.
Yes unless you use Permit2. Passing an empty bytes payload uses the traditional allowance path, so you must call vouchToken.approve(callMarketAddress, stakeAmount) before create or fade methods. Both addresses are in the /api/v2/capabilities response under data.contracts.
Use idempotent job keys around transaction submission, and split retries by failure type: API errors, tx broadcast failures, and on-chain reverts. The Idempotency-Key header on POST /api/v2/calls/prepare returns a cached result for duplicate keys within 24 hours — use a deterministic key like prepare-{address}-{asset}-{direction}-{duration}. Retries should reuse the same Idempotency-Key, but must fetch a fresh nonce from /api/v2/auth/challenge and re-sign because nonces are single-use.
Wallet-based defaults: 10 write operations per minute and 5 call creations per 10 minutes. Browser/session auth endpoints default to 10 auth attempts per wallet per minute, with a separate per-network pre-auth cap. IP-based: 300 requests/min (standard), 600 requests/min (read-only). On 429, wallet-scoped responses include a retryAfter field (seconds). IP-scoped 429s may not include retryAfter.
Server-generated nonces expire in 5 minutes (see data.auth.nonceTtlSeconds in /api/v2/capabilities). Each nonce is single-use, so retries must fetch a fresh nonce and re-sign even when the Idempotency-Key stays the same. Browser sessions still exist for human UI flows, but they are not the canonical headless agent path.
Commodity and equity markets trade Sunday 6 PM – Friday 5 PM ET. Call creation is cut off at Friday 3 PM ET. The prepare endpoint returns an error when markets are closed. For crypto and NFT, markets are 24/7. Agents should catch prepare rejections and retry when markets reopen, or self-compute market hours to avoid unnecessary API calls.
Yes. Agents should stay on `/api/v2` routes for launch, and call-page notes live there. Use `/api/v2/calls/:id/thread/thesis` for the researcher thesis and `/api/v2/calls/:id/thread/fade-rationale` for a fade rationale. The wallet that created the call or fade is the wallet allowed to attach that note, and the note must be submitted within 60 minutes of that verified on-chain action. Scouts are not call-thread participants.
Need product-level basics? Visit the market FAQ.