Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e0314d4
fix(agents): restore valid package.json after the agentd merge
ExcelDsigN-tech Oct 21, 2026
607a0ce
fix(agents): repair the agentd merge so the agents package typechecks
ExcelDsigN-tech Oct 21, 2026
d05397e
fix(agents): close the multiformats entry in package-lock.json
ExcelDsigN-tech Oct 21, 2026
94bc145
feat(agents): typed, validated agent config with network safety inter…
ExcelDsigN-tech Oct 21, 2026
d5a0db1
docs(agents): document every agent config variable and the mainnet in…
ExcelDsigN-tech Oct 21, 2026
0fea17e
fix(agents): restore valid package.json after the agentd merge
ExcelDsigN-tech Oct 21, 2026
38aabaf
fix(agents): repair the agentd merge so the agents package typechecks
ExcelDsigN-tech Oct 21, 2026
4592de7
fix(agents): close the multiformats entry in package-lock.json
ExcelDsigN-tech Oct 21, 2026
918ac51
fix(agents): let the adapter reach http RPC URLs
ExcelDsigN-tech Oct 21, 2026
5ebdf8e
fix(agents): unwrap quote_buy's Result in StellarAdapter.quoteBuy
ExcelDsigN-tech Oct 21, 2026
9ba1f90
test(agents): add a deterministic fake Soroban RPC with fault injection
ExcelDsigN-tech Oct 21, 2026
587c377
test(agents): chaos suite for the live adapter and trader/maker flows
ExcelDsigN-tech Oct 21, 2026
c5d83c3
ci: run the agents chaos suite in the Node job
ExcelDsigN-tech Oct 21, 2026
c3c71ea
Merge branch 'feat/chaos-tests-fake-rpc' into feat/agent-config-chaos…
ExcelDsigN-tech Oct 21, 2026
b7df4f8
test(agents): tie Date.now to the fake ledger clock in chaos runs
ExcelDsigN-tech Oct 21, 2026
3488702
feat(agents): event-driven triggers with cursor checkpointing
ExcelDsigN-tech Oct 21, 2026
a33f17f
docs(agents): document event triggers vs the indexer, EVENTS_* settin…
ExcelDsigN-tech Oct 21, 2026
ad537b2
feat(agents,model): collateralForShares, the exact buy-side FPMM inverse
ExcelDsigN-tech Oct 21, 2026
f9fb037
feat(agents): cross-market arbitrage with void-aware planning and jou…
ExcelDsigN-tech Oct 21, 2026
f3fc5d4
docs(agents): document arbitrage configuration and risks, ARB_* setti…
ExcelDsigN-tech Oct 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,11 @@ jobs:
npm install
npm run build
npm test

# Fake-RPC fault injection against the real live adapter (#106). Uses
# the wasm downloaded above when it fits the adapter, else the vendored
# copies; deterministic (seeded faults, fake ledger clock), no network.
- name: agents — chaos tests
run: |
cd agents
npm run test:chaos
121 changes: 101 additions & 20 deletions oracledesk-stellar/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,23 @@
# Testnet only — nothing here should ever point at mainnet or hold real funds.

# --- Stellar network -----------------------------------------------------
# The agents validate everything below at startup (agents/core/config.ts) and
# report every problem at once. `npm run trader -- --print-config` prints the
# resolved config with secrets redacted and exits without network access.
#
# STELLAR_NETWORK: testnet (default when unset) | futurenet | local | mainnet.
# STELLAR_NETWORK_PASSPHRASE is optional; if set it must equal the canonical
# passphrase for STELLAR_NETWORK or startup fails:
# testnet "Test SDF Network ; September 2015"
# futurenet "Test SDF Future Network ; October 2022"
# local "Standalone Network ; February 2017"
# mainnet "Public Global Stellar Network ; September 2015"
# STELLAR_RPC_URL defaults per network (testnet https://soroban-testnet.stellar.org,
# futurenet https://rpc-futurenet.stellar.org, local http://localhost:8000/soroban/rpc).
# Mainnet has no default and must be set explicitly.
STELLAR_NETWORK=testnet
STELLAR_RPC_URL=https://soroban-testnet.stellar.org
# STELLAR_NETWORK_PASSPHRASE=Test SDF Network ; September 2015

# Portion of each trading fee kept by the protocol treasury (bps), set on
# market-core at deploy time. The remainder accrues to the market creator
Expand Down Expand Up @@ -32,10 +47,18 @@ AGENT_IDENTITY=oracledesk-agent
# TRADER_MAX_DAILY=5000000000

# --- Deployed contract addresses ------------------------------------------
# Populated by scripts/deploy-testnet.sh into deployments/<network>.json
# (testnet by default, local when NETWORK=local). Only set these to override.
# DEPLOYMENTS_PATH selects a different record for the agents:
# DEPLOYMENTS_PATH=deployments/local.json
# Populated by scripts/deploy-testnet.sh into deployments/<network>.json.
# The agents read deployments/<STELLAR_NETWORK>.json unless DEPLOYMENTS_FILE
# (or its older alias DEPLOYMENTS_PATH; if both are set they must match) names
# another record. The record is schema-checked: deployer, agent (and optional
# agents.maker / agents.trader) must be G... keys, and contracts.usdc,
# market_core, resolver, treasury and reasoning_registry must all be present
# C... ids, or the record is reported as stale. A record without a "network"
# field is treated as testnet; one with it must match STELLAR_NETWORK.
# DEPLOYMENTS_FILE=deployments/local.json
#
# Each *_CONTRACT_ID below, when set to a C... id, replaces that contract in
# the record after validation, and startup logs the override. Blank = unset.
# MARKET_CORE_CONTRACT_ID=
# RESOLVER_CONTRACT_ID=
# TREASURY_CONTRACT_ID=
Expand All @@ -61,29 +84,87 @@ TESTNET_FACILITATOR_API_KEY=
# to use here; none is hardcoded anywhere in this repo. -->

# --- Agents (agents/) -------------------------------------------------------
# "dry-run" (default, never signs or submits) or "live" (real testnet txs).
# Numbers are parsed strictly: base-10 integers only. A variable that is set
# but empty, fractional, negative, NaN, hex or out of range fails startup;
# only an unset variable takes its default.
#
# AGENT_MODE: "dry-run" (default only when unset; never signs or submits) or
# "live" (real transactions). Any other value, including "", is rejected.
AGENT_MODE=dry-run
# Signing keys (S...). Each CLI prefers its own role key and falls back to
# the shared AGENT_SECRET_KEY (the constructor agent, which has every role):
# bin/market-maker.ts reads MAKER_SECRET_KEY, bin/trader.ts TRADER_SECRET_KEY.
# Treasury calls are made as that key's address.
# the shared AGENT_SECRET_KEY: bin/market-maker.ts uses MAKER_SECRET_KEY,
# bin/trader.ts TRADER_SECRET_KEY. Live mode requires one, and its public key
# must equal the role's address in the deployment record (agents.maker /
# agents.trader, else agent). Dry-run never passes the key to the adapter.
# Blank = unset; a non-blank value must be a valid S... key in either mode.
AGENT_SECRET_KEY=
MAKER_SECRET_KEY=
TRADER_SECRET_KEY=
# DEPLOYMENTS_PATH=deployments/testnet.json
# Local E2E / live-trader overrides (see scripts/e2e-local.sh):
# TRADER_MARKET_ID=0
# TRADER_MARKET_YES_BPS=4000
# TRADER_FAIR_VALUE_BPS=6000
# Optional: an off-chain reference price source for the trader's fair-value
# estimate. Read-only; never used for execution (see docs/architecture.md).
POLYMARKET_READ_ONLY_API_URL=
# Execution bounds for real trades (agents/bin/trader.ts). The trader quotes
# first, then sends min_shares_out / limit_price_bps derived from the quote:
# SLIPPAGE_BPS is how far below the quoted average price the fill may land,
# DEADLINE_SECS is how long the signed trade stays valid after submission.
# Startup probes check that STELLAR_RPC_URL serves STELLAR_NETWORK's
# passphrase and that every contract in the record exists and is not
# archived. Mandatory in live mode (failure stops startup); best effort in
# dry-run (failures are warnings). `--offline` skips them, in dry-run only.
# Timeout per probe, 1-600000 ms:
AGENT_STARTUP_PROBE_TIMEOUT_MS=5000
# Trader. MIN_EDGE_BPS: 0-10000 (default 500). TRADE_SIZE_STROOPS: 1 up to
# the i128 maximum (default 100000000 = 10 USDC). FAIR_VALUE_BPS: 0-10000;
# unset = each market's own price (no external oracle is wired in yet).
# MIN_EDGE_BPS=500
# TRADE_SIZE_STROOPS=100000000
# FAIR_VALUE_BPS=
# Execution bounds for real trades. The trader quotes first, then sends
# min_shares_out / limit_price_bps derived from the quote: SLIPPAGE_BPS
# (0-10000) is how far below the quoted average price the fill may land,
# DEADLINE_SECS (1-86400) is how long the signed trade stays valid.
SLIPPAGE_BPS=50
DEADLINE_SECS=60
# Market maker. CONFIDENCE_THRESHOLD_BPS: 0-10000 (default 1000).
# DEFAULT_SEED_STROOPS: at least 1000000000 (100 USDC; default 1500000000).
# MARKET_CLOSE_IN_SECS: 1 to one year (default 3600).
# CONFIDENCE_THRESHOLD_BPS=1000
# DEFAULT_SEED_STROOPS=1500000000
# MARKET_CLOSE_IN_SECS=3600
# Where reasoning traces are written (non-empty path, default ./.traces).
# TRACE_STORE_DIR=./.traces
#
# Event triggers for `npm run trader -- --follow` (#105). The trader polls
# contract events and re-evaluates only the markets they point at.
# EVENTS_STATE_DIR: checkpoints + dead-letter.jsonl, one checkpoint file
# per network and contract set (default ./.events, gitignored).
# EVENTS_TRADE_THRESHOLD: a trade of at least this many stroops queues its
# market for re-evaluation (default 500000000 = 50 USDC).
# EVENTS_POLL_INTERVAL_MS: 100-3600000 (default 10000).
# EVENTS_PAGE_LIMIT: events per getEvents page, 1-10000 (default 100).
# EVENTS_CONCURRENCY: markets handled in parallel, 1-64 (default 4).
# EVENTS_MAX_ATTEMPTS: tries per handler before dead-lettering, 1-100 (default 5).
# EVENTS_DISPUTE_WINDOW_SECS: dispute window used for proposal alerts (default 600).
# EVENTS_STATE_DIR=./.events
# EVENTS_TRADE_THRESHOLD=500000000
#
# Cross-market arbitrage (#108), run by the trader when ARB_ENABLED=1 (or
# true). See docs/architecture.md, "Cross-market arbitrage". Dry-run only
# plans and logs; live trades both legs sequentially.
# ARB_RELATIONS_FILE: required when enabled; JSON list of relations.
# ARB_JOURNAL_FILE: crash-recovery journal (default ./.arb/journal.jsonl).
# ARB_MAX_NOTIONAL: most one plan may spend across both legs, stroops (default 1000000000).
# ARB_MIN_PROFIT_STROOPS: smallest guaranteed profit worth trading (default 1000000).
# ARB_MIN_PROFIT_BPS: smallest profit as a share of cost, 0-10000 (default 50).
# ARB_MAX_UNHEDGED_STROOPS: cap on the first leg, left naked if the second fails (default 500000000).
# ARB_MARKET_HEADROOM_BPS: share of each market's max_market kept free, 0-9999 (default 2000).
# ARB_MAX_LEG_LOSS: how much more the second leg may cost than planned before unwinding (default 10000000).
# ARB_MAX_OPEN_LEGS: arbitrage legs in flight at once, 2-64 (default 2).
# ARB_MAX_UNWIND_RETRIES: attempts to sell back a stranded first leg, 1-20 (default 3).
# ARB_ENABLED=0
# ARB_RELATIONS_FILE=./arb-relations.json
#
# Mainnet interlock. The agents refuse STELLAR_NETWORK=mainnet unless ALL of:
# - STELLAR_NETWORK_PASSPHRASE is the public network passphrase (or unset),
# - STELLAR_RPC_URL is set explicitly,
# - the deployment file is not testnet.json,
# - the deployment record declares "network": "mainnet",
# - AGENT_ALLOW_MAINNET is exactly I_ACCEPT_REAL_FUNDS_
# Every unmet condition is reported. Leave this unset on testnet.
# AGENT_ALLOW_MAINNET=

# x402: strict startup by default; false drops unavailable networks from accepts.
X402_REQUIRE_ALL_NETWORKS=true
Expand Down
5 changes: 5 additions & 0 deletions oracledesk-stellar/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ yarn-error.log*
.env.*
!.env.example

# Agent event-trigger checkpoints and dead letters (EVENTS_STATE_DIR, #105)
.events/
# Arbitrage execution journal (ARB_JOURNAL_FILE, #108)
.arb/

# Stellar / Soroban CLI local state (funded testnet identities, RPC caches)
.soroban/
.stellar/
Expand Down
47 changes: 29 additions & 18 deletions oracledesk-stellar/agents/bin/agentd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ import { Runtime, type Task } from "../runtime/runtime.js";
import type { StateStore } from "../runtime/state.js";
import { makerTask } from "../runtime/tasks/maker.js";
import { traderTask } from "../runtime/tasks/trader.js";
import { buildAdapter, createLease, createStateStore, loadDeployments, makerDeps, runtimeOptions, traderDeps } from "../runtime/wiring.js";
import { loadAgentConfig } from "../core/config.js";
import { buildAdapter, createLease, createStateStore, makerDeps, runtimeOptions, traderDeps } from "../runtime/wiring.js";

export interface AgentdArgs {
once: boolean;
Expand All @@ -46,28 +47,35 @@ export function parseArgs(argv: string[]): AgentdArgs {

export interface AgentdOverrides {
/** Task factories by name (tests inject fakes; default: maker, trader). */
taskFactories?: Record<string, (env: NodeJS.ProcessEnv) => Task>;
taskFactories?: Record<string, TaskFactory>;
state?: StateStore;
lease?: Lease;
log?: Logger;
/** Installs SIGINT/SIGTERM handlers (default true outside --once). */
handleSignals?: boolean;
}

function defaultFactories(): Record<string, (env: NodeJS.ProcessEnv) => Task> {
type TaskFactory = (env: NodeJS.ProcessEnv) => Task | Promise<Task>;

/** Each real task validates its own role's config (core/config.ts), including
* the network probes and mainnet interlock; `scope` records the treasury the
* state store is keyed by. */
function defaultFactories(scope: { treasury?: string }): Record<string, TaskFactory> {
return {
maker: (env) => {
const deployments = loadDeployments();
const adapter = buildAdapter("maker", parseAgentMode(env), env, deployments);
return makerTask(makerDeps(env, adapter, deployments), {
maker: async (env) => {
const cfg = await loadAgentConfig({ role: "maker", env, argv: [] });
scope.treasury = cfg.deployments.contracts.treasury;
const adapter = buildAdapter(cfg);
return makerTask(makerDeps(cfg, adapter, cfg.deployments), {
intervalMs: intEnv(env, "MAKER_INTERVAL_MS", 5 * 60_000),
jitterMs: intEnv(env, "MAKER_JITTER_MS", 15_000),
});
},
trader: (env) => {
const deployments = loadDeployments();
const adapter = buildAdapter("trader", parseAgentMode(env), env, deployments);
return traderTask(traderDeps(env, adapter, deployments, () => Math.floor(Date.now() / 1000)), {
trader: async (env) => {
const cfg = await loadAgentConfig({ role: "trader", env, argv: [] });
scope.treasury = cfg.deployments.contracts.treasury;
const adapter = buildAdapter(cfg);
return traderTask(traderDeps(cfg, adapter, cfg.deployments, () => Math.floor(Date.now() / 1000)), {
intervalMs: intEnv(env, "TRADER_INTERVAL_MS", 60_000),
jitterMs: intEnv(env, "TRADER_JITTER_MS", 5_000),
});
Expand All @@ -85,13 +93,16 @@ export async function runAgentd(argv: string[], baseEnv: NodeJS.ProcessEnv, o: A
const overrides = args.config ? (JSON.parse(readFileSync(args.config, "utf8")) as Record<string, unknown>) : {};
const env: NodeJS.ProcessEnv = { ...baseEnv, ...Object.fromEntries(Object.entries(overrides).map(([k, v]) => [k, String(v)])) };
const mode = parseAgentMode(env);
const factories = o.taskFactories ?? defaultFactories();
const tasks = args.tasks.map((name) => {
const make = factories[name];
if (!make) throw new ConfigError(`unknown task ${name} (known: ${Object.keys(factories).join(", ")})`);
return make(env);
});
const scope = o.taskFactories ? "agentd" : loadDeployments().contracts.treasury;
const real: { treasury?: string } = {};
const factories = o.taskFactories ?? defaultFactories(real);
const tasks = await Promise.all(
args.tasks.map((name) => {
const make = factories[name];
if (!make) throw new ConfigError(`unknown task ${name} (known: ${Object.keys(factories).join(", ")})`);
return make(env);
}),
);
const scope = o.taskFactories ? "agentd" : real.treasury ?? "agentd";
runtime = new Runtime({
tasks,
state: o.state ?? (await createStateStore(mode, env, scope)),
Expand Down
Loading
Loading