Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
86 changes: 79 additions & 7 deletions CLAUDE.md

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Nyx Foundation

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ For details, see [Backtesting](docs/guide/backtest.md).
| [Backtesting](docs/guide/backtest.md) | Replaying state dump + official regimes, iterating with `--repeat`, sparring, what is and isn't measurable |
| [Run Output and Analysis](docs/guide/run-output.md) | The output files under `runs/<id>/` and how to analyze a run afterwards |
| [Protocols and Actions](docs/guide/protocols-and-actions.md) | Reference: actions per venue, stablecoin accounting, oracle control |
| [Self-improving Agents](docs/guide/llm-agents.md) | agent.ts + improve.md (in-run strategy rewriting, sandbox, rollback, frozen control) |
| [Self-improving Agents](docs/guide/llm-agents.md) | agent.ts + prompt.md (in-run strategy rewriting, sandbox, rollback, frozen control) |

**How the environment works / operations**:

Expand All @@ -151,6 +151,20 @@ For details, see [Backtesting](docs/guide/backtest.md).

---

## License

MIT — see [LICENSE](LICENSE).

That is the answer to the question this repository is built around: **copy `example/agents/<id>/`,
change it, keep what you build.** A strategy written from one of the bundled agents is yours, and
nothing here asks for it back.

A few files in the tree are somebody else's work and keep their own terms — the canonical WETH9 mock
and Curve's prebuilt artifacts. [THIRD-PARTY.md](THIRD-PARTY.md) lists them, along with the
dependencies `deployer/scripts/setup-vendors.sh` fetches at setup rather than redistributing.

---

## Disclaimer

This is an **MVP / Proof of Concept** for research and experimentation, not intended for production use. The Aave / GMX oracles are mocks controlled by the coordinator, and the fair price is a synthetic path generated deterministically. Simulation results (PnL, ranking, discrimination) depend on the environment configuration, SEED, and sample count, and do not guarantee real-market performance.
Expand Down
43 changes: 43 additions & 0 deletions THIRD-PARTY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Third-party code

The repository is MIT (see [LICENSE](LICENSE)). That covers everything written here. Three things in
the tree are somebody else's work and keep their own terms, and this file records which — so that
"the repository is MIT" is a statement about the code we wrote rather than about every byte in it.

## Redistributed here

**`deployer/contracts/WETH9.sol` — GPL-3.0.** The canonical WETH9, kept verbatim (its own
`SPDX-License-Identifier: GPL-3.0` header is intact) so that the local chain's wrapped ether behaves
byte-for-byte like the deployed one every venue integrates against. It is a development mock, never
part of a distributed agent bundle.

**`deployer/vendor/curve/*.json` — compiled Curve contracts.** ABI and bytecode only, committed
because Curve ships prebuilt artifacts and rebuilding Vyper is not reproducible without a pinned
toolchain. Upstream, recorded in `deployer/scripts/setup-vendors.sh`:

| file | upstream |
|---|---|
| `CurveStableSwapNG*.json` | [curvefi/stableswap-ng](https://github.com/curvefi/stableswap-ng) |
| `CurveTwocrypto*.json` | [curvefi/twocrypto-ng](https://github.com/curvefi/twocrypto-ng), tag `lite-0.3.10` |

**`deployer/vendor/gmx-localhost.patch` and `deployer/vendor/aave/*`** — a patch and a build
configuration authored here against those projects' sources.

## Fetched, not redistributed

`deployer/scripts/setup-vendors.sh` clones the following at pinned commits when the deployer is first
set up. None of it is in this repository (`deployer/.gitignore`), and each carries its own licence:

| clone | project |
|---|---|
| `vendor/liquity-src/` | Liquity V1 (all 81 contract sources are `SPDX-License-Identifier: MIT`) |
| `vendor/gmx-src/` | GMX V2 (gmx-synthetics) |
| `vendor/curve-src/`, `vendor/twocrypto-src/` | Curve, for rebuilding the artifacts above |

npm dependencies are likewise resolved at install time and are not redistributed here.

## Agent bundles

`npm run bundle:agent <id>` packages the runtime, the SDK, `example/agents/lib/` and one agent
directory. Everything it contains is this repository's own code, under MIT — a participant may copy,
modify and keep whatever they build from it.
38 changes: 30 additions & 8 deletions config/example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
# start `npm run anvil` in another terminal with ARB_RPC_URL set.
#
# The agents below are rule strategies (agent.ts) that trade every block. Three of them also ship an
# improve.md, so an LLM periodically rewrites the strategy while it runs (ADR 0018) -- the LLM is
# prompt.md, so an LLM periodically rewrites the strategy while it runs (ADR 0018) -- the LLM is
# never in the trade path. Prompt mode, where an LLM produced each action, was removed: it managed
# one decision every 8-28 blocks and 1/64 the actions of the same strategy in rule mode.
#
Expand All @@ -37,13 +37,14 @@ run:
seed: 1
# The trading loop is rule-speed (one decision per block) regardless of the LLM, so the run length
# is not set by LLM latency any more. It does need to be long enough for at least one revision to
# land -- improve.md declares reviseEveryBlocks: 60.
# land -- prompt.md declares reviseEveryBlocks: 60.
blocks: 100
seconds: 300
blockTimeSec: 2
# gmx and aave are left out because they are the slow half of the deploy; add them once
# `deployer` has run with them. `lst` is local-only (issue #38) and fails fast on a fork.
protocols: [uniswap, balancer, curve, lst]
# `deployer` has run with them. `lst` (issue #38) and `liquity` (issue #39) are local-only and
# fail fast on a fork.
protocols: [uniswap, balancer, curve, lst, liquity]
economicGas: false
localDeploy: true
reportDir: ./runs
Expand Down Expand Up @@ -97,9 +98,26 @@ flow:
# To trade multi-asset (WBTC), enable WBTC flow (creates price divergence = arbitrage opportunities):
# baseMax: { WBTC: "50000000" }

# stress (market stress events. default off):
# stress:
# events:
# stress (market stress events).
#
# The eUSD depeg is here rather than commented out because the CDP venue is otherwise correctly
# inert: the deploy seeds the eUSD/USDC pool at par, and redemption costs a 50bps floor fee, so with
# nothing pushing the peg the right move is to sit still and redemption-arb would do exactly that.
# The event has the environment sell eUSD into its own market for the length of a window and buy it
# back afterwards. See config/liquity.yaml for how the magnitude range was calibrated.
#
# Uncomment the crash below to add a price gap as well (ADR 0009); `victimCount` stages the Aave
# positions a liquidator agent could take.
stress:
events:
- {
type: eusdDepeg,
magnitudeRange: [0.4, 0.6],
windowFrac: [0.25, 0.6],
rampBlocks: 4,
holdBlocks: 10,
decayBlocks: 12,
}
# - { type: crash, magnitudeRange: [0.12, 0.16], windowFrac: [0.3, 0.7], rampBlocks: 3, holdBlocks: 6, decayBlocks: 8 }
# victimCount: 0

Expand All @@ -113,7 +131,7 @@ agents:
description: does nothing (baseline; rule-based on purpose)
- id: venue-arb
wallet: AGENT2_PRIVATE_KEY
description: WETH-only cross-venue arbitrage, with an LLM revising it in-run (improve.md)
description: WETH-only cross-venue arbitrage, with an LLM revising it in-run (prompt.md)
env: { ERIS_IMPROVE_LOG_CALLS: "1" }
# Claude Code / Codex subscription instead of Ollama (no API key):
# env: { ERIS_IMPROVE_LOG_CALLS: "1", ERIS_LLM_MODEL: "claude-cli:haiku" }
Expand All @@ -130,5 +148,9 @@ agents:
wallet: AGENT5_PRIVATE_KEY
description: liquid staking — stake for yield, or trade the LST redemption/market gap, revised in-run
env: { ERIS_IMPROVE_LOG_CALLS: "1" }
- id: redemption-arb
wallet: AGENT6_PRIVATE_KEY
description: CDP stablecoin — buy eUSD below par and redeem it against the riskiest Trove, revised in-run
env: { ERIS_IMPROVE_LOG_CALLS: "1" }
# config/lst.yaml is the same venue with a second competing participant and the calibration knobs
# spelled out, if you want to look at the LST market on its own.
2 changes: 1 addition & 1 deletion config/lst.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ agents:
wallet: AGENT1_PRIVATE_KEY
baseline: true
description: does nothing (baseline)
# Self-improving (agent.ts + improve.md, ADR 0018): the strategy trades every block and an LLM
# Self-improving (agent.ts + prompt.md, ADR 0018): the strategy trades every block and an LLM
# periodically rewrites it. Needs OLLAMA_API_KEY in .env.local, or ERIS_LLM_MODEL: "claude-cli"
# for a subscription CLI. Without a backend the run still completes — the revisions are recorded
# as failed and the strategy keeps trading unchanged.
Expand Down
90 changes: 90 additions & 0 deletions config/regimes/liquity-crash.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# config/regimes/liquity-crash.yaml — the CDP venue's borrower and underwriter sides (issue #39).
#
# Not part of the ADR 0017 competition set. `liquity` covers the venue's α (buy a depegged eUSD and
# redeem it); this one covers the other half, which only exists when the collateral price falls:
#
# trove-manager holds a Trove through the path, against liquidation, redemption and Recovery Mode
# sp-underwriter deposits eUSD to absorb what falls under MCR, calls the liquidation, banks the ETH
#
# Run: npm run backtest -- --regime liquity-crash --seed 501 [--agents <roster>]
#
# **Two Troves that differ in one switch.** `trove-manager` holds the eUSD it draws, so repaying is
# always available as a defence; `trove-manager-spent` sells it for USDC and posts every last WETH,
# so when the crash comes it has neither. That is the regime's question -- what a CDP borrower's
# defences are actually worth -- and it is why one of them gets liquidated and the other does not.
#
# Funding is WETH-heavy because a Trove needs collateral. That reintroduces price drift into
# netPnlUsdc; read alphaUsdc, and read scores within a run rather than across price paths.
#
# **Recovery Mode is not reachable here** and the regime does not pretend otherwise. The genesis
# Trove is 250 ETH against 250k eUSD (300% ICR) and dwarfs anything agents can open with this
# funding: measured at seed 501, the system TCR bottoms at 2.244 against a CCR of 1.5. Reaching it
# needs the system's debt roughly tripled, which dilutes the redemption fee curve and the Stability
# Pool's relative depth -- both of them properties #39 measured -- so it is issue #59 rather than a
# constant in this file.

run:
blocks: 120
seconds: 900 # a stress run ends by block count (ADR 0009)
blockTimeSec: 2 # fixed to the regime (ADR 0016 §2)
# uniswap is not optional: both the Stability Pool and a closed Trove pay out in native ETH, and
# without a spot market an agent has to keep the exposure instead of banking it.
protocols: [uniswap, balancer, curve, liquity]
economicGas: false
localDeploy: true
reportDir: ./runs

funding:
# A Trove needs collateral, so unlike the evaluation profile this hands out WETH (ADR 0017 §4
# applies to the competition set; this regime is venue verification).
wethWei: "20000000000000000000" # 20 WETH
usdcUnits: "25000000000"

limits:
agentWethWei: "5000000000000000000"
agentUsdcUnits: "5000000000"

flow:
uninformedMaxWethWei: "1000000000000000000"
informedMaxWethWei: "2000000000000000000"
balancerMaxWethWei: "1000000000000000000"
curveMaxWethWei: "1000000000000000000"
informedArbFeeBps: 30
uninformedArrivalRate: "0.9"
uninformedSizeSigma: "1.0"

stress:
events:
# Deep enough to take a thin Trove through MCR, and no deeper. `trove-manager-spent` opens at
# 125%, so 15% puts it at 106% and 22% at 98% -- under the 110% floor across the whole range,
# which is what makes the liquidation path a property of the regime rather than of the seed.
# The Trove that kept its eUSD sits at 200% and comes through every draw.
- {
type: crash,
magnitudeRange: [0.15, 0.22],
windowFrac: [0.3, 0.65],
rampBlocks: 4,
holdBlocks: 12,
decayBlocks: 15,
}

agents:
- id: noop
wallet: AGENT1_PRIVATE_KEY
baseline: true
description: does nothing (baseline)
- id: trove-manager
wallet: AGENT2_PRIVATE_KEY
description: borrows at 200% and keeps the eUSD, so repayment stays available as a defence
- id: trove-manager-spent
dir: trove-manager
wallet: AGENT3_PRIVATE_KEY
description: borrows at 125%, posts everything and sells the eUSD — the borrower with no defences left
env:
ERIS_TROVE_TARGET_ICR: "1.25"
ERIS_TROVE_FLOOR_ICR: "1.15"
ERIS_TROVE_OPEN_BPS: "10000"
ERIS_TROVE_SPEND_DEBT: "1"
- id: sp-underwriter
wallet: AGENT4_PRIVATE_KEY
description: underwrites the Stability Pool, calls the liquidation and banks the collateral
87 changes: 87 additions & 0 deletions config/regimes/liquity.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# config/regimes/liquity.yaml — the CDP stablecoin venue (issue #39). ADR 0016 §2
#
# Not part of the ADR 0017 competition set (the seven regimes there predate this venue). Kept as an
# official regime for venue-level verification, and it follows the same discipline: ranges rather
# than values, blockTimeSec fixed to the production value, seed supplied at run time.
# Run: npm run backtest -- --regime liquity --seed 401 [--agents <roster>]
#
# What this regime tests that the others do not: a dislocation against a price the protocol
# *enforces* rather than one the market has to agree on. eUSD is always exchangeable for $1 of
# collateral against the riskiest Trove, so a discount on its pool is an α with a floor under it —
# bounded by a redemption fee that every redemption in the run raises for everyone after it.
#
# Funding stays USDC-only (ADR 0017 §4): the whole loop is USDC -> eUSD -> ETH -> USDC, so nobody
# has to start with price exposure to trade it.

run:
blocks: 120 # long enough for the depeg window plus the recovery after it
seconds: 900 # a stress run ends by block count (ADR 0009)
blockTimeSec: 2 # fixed to the regime (ADR 0016 §2)
# uniswap is not optional: redemption pays native ETH, and without a spot market an agent has to
# keep the price exposure it just earned rather than banking the arb.
protocols: [uniswap, balancer, curve, liquity]
economicGas: false
localDeploy: true # backtest runs on a local anvil loaded with the distributed state
reportDir: ./runs

funding:
wethWei: "0" # USDC-only: netPnlUsdc carries β, so nobody may start already exposed
usdcUnits: "25000000000"

limits:
agentWethWei: "1000000000000000000"
agentUsdcUnits: "5000000000"

flow:
uninformedMaxWethWei: "1000000000000000000"
informedMaxWethWei: "2000000000000000000"
balancerMaxWethWei: "1000000000000000000"
curveMaxWethWei: "1000000000000000000"
informedArbFeeBps: 30
uninformedArrivalRate: "0.9"
uninformedSizeSigma: "1.0"
# There is no eUSD flow bot: the peg moves only when the stress event below pushes it or an agent
# trades it. That is why the event is part of the regime rather than optional decoration — at par
# the venue is correctly inert.

stress:
events:
# Calibration, measured on the deployed pool (100k/100k, A=100). magnitude is the fraction of
# its seeded eUSD depth the environment has sold at the top of the trapezoid, and what that
# produces is the *marginal* discount an agent would trade against: 40k sold moves it 114bps,
# 50k moves it 175bps, 60k moves it 282bps.
#
# The costs it has to clear: a 50bps redemption fee floor, ~30bps to sell the redeemed ETH back
# to USDC, and whatever safety margin the agent keeps. So the low end of this range barely pays
# and the high end pays well -- and the trapezoid's ramp passes through every level in between,
# where the right move is to wait. A range where every draw pays would measure reaction time
# rather than judgement.
#
# One more thing this scale makes sharp: with a 250k eUSD supply, Liquity's baseRate rises about
# 100bps per 5k redeemed, so the *first* redemption is cheap and the next one may not clear at
# all. That is the venue's own timing pressure, not a calibration artefact.
- {
type: eusdDepeg,
magnitudeRange: [0.4, 0.6],
windowFrac: [0.25, 0.6],
rampBlocks: 4,
holdBlocks: 12,
decayBlocks: 15,
}

agents:
- id: noop
wallet: AGENT1_PRIVATE_KEY
baseline: true
description: does nothing (baseline)
- id: redemption-arb
wallet: AGENT2_PRIVATE_KEY
description: buys eUSD below par and redeems it against the riskiest Trove
- id: redemption-arb-eager
dir: redemption-arb
wallet: AGENT3_PRIVATE_KEY
description: the same strategy with a thinner safety margin
env: { ERIS_REDEMPTION_SAFETY_BPS: "5" }
- id: venue-arb
wallet: AGENT4_PRIVATE_KEY
description: WETH-only cross-venue arbitrage (keeps the AMM venues honest)
1 change: 1 addition & 0 deletions core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "@eris/core",
"version": "0.1.0",
"private": true,
"license": "MIT",
"type": "module",
"description": "eris-competition environment daemon + scoring (ADR 0015). Off-limits to participants. Importing from example is forbidden (checked by scripts/checkImportBoundaries.ts).",
"dependencies": {
Expand Down
1 change: 1 addition & 0 deletions core/src/backtest/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ const VENUE_TO_DEPLOYMENT_KEY: Record<string, string> = {
gmx: "gmxV2",
aave: "aaveV3",
lst: "lst",
liquity: "liquity",
};

// Whether the venues the regime requires are all present in the state dump (deployments bundled in
Expand Down
2 changes: 1 addition & 1 deletion core/src/realtime/agentProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export class RealtimeAgentProcess {
args = spec.args ?? [];
} else {
// Convention resolution (ADR 0015 §6): id (or the dir override) points to <agentsDir>/<dir>/, and
// bot.ts drives its contents (agent.ts decide/run, plus improve.md when self-improving).
// bot.ts drives its contents (agent.ts decide/run, plus prompt.md when self-improving).
const agentDir = resolve(agentsDir, spec.dir ?? spec.id);
if (!existsSync(agentDir)) {
throw new Error(
Expand Down
Loading
Loading