Skip to content

Repository files navigation

Nectar

A self-funding strategy factory on Circle's Arc. An agent invents trading strategies, proves them out-of-sample, commits the survivors on-chain before they touch capital, and pays for its own thinking — per LLM call, per data pull, per backtest — in USDC.

Users can submit strategies too, as Python or as a sentence. Both doors lead to the same gauntlet.

pnpm install && pnpm demo

No Circle key. No Anthropic key. No StableFX KYB. No funded wallet. No network. Everything that would need one has a mock behind the same interface, and every simulated component is badged as such in the UI rather than quietly implied to be real.


The three problems

Strategy creation is gated. People have ideas — "buy EURC when it dips 20 bps below fair value" — and no infrastructure to express, test or run them safely. Nectar takes that sentence, compiles it, reads back what the code actually does, and gives you an honest walk-forward report.

Backtests are unverifiable. Every trading product says "backtested returns of X%". There is no way to check that the deployed code is the code that was tested, or that the window wasn't cherry-picked. Nectar commits keccak256(source) plus the dataset fingerprint and the out-of-sample metrics to a registry contract before a strategy is allowed near capital. Anyone can re-hash the published module and check it.

Autonomous agents can't sustain themselves. An AI trading agent burns inference and market data continuously. On traditional rails, per-call payment is impossible — minimum fees dwarf per-call value — so agents run on prepaid accounts with no cost discipline. Arc's Nanopayments make $0.002 per inference a real transfer, which turns "is this agent economically viable?" into a number you can watch move.


Architecture

flowchart TB
    subgraph intake["Two intake doors"]
        forge["<b>Forge</b><br/>Claude, or a 12-family<br/>template library + mutation"]
        user["<b>/submit</b><br/>Python paste, or plain English<br/>→ compile → <i>read-back</i> → confirm"]
    end

    subgraph gauntlet["One gauntlet — identical for both"]
        direction TB
        validate["<b>AST allowlist</b><br/>no imports · no I/O · no dunders<br/>no eval · no unbounded loops"]
        sandbox["<b>Sandboxed subprocess</b><br/>rlimits: CPU · memory · fsize=0<br/>poisoned module table · no network"]
        backtest["<b>Walk-forward backtest</b><br/>out-of-sample only · signal at i fills at i+1<br/>spread + sqrt impact + USDC gas"]
        hurdle{"<b>Beats USYC + costs?</b><br/>read on-chain, not configured"}
        paper["<b>Paper arena</b><br/>live feed, simulated fills<br/>must keep its backtest character"]
    end

    subgraph chain["Arc"]
        registry[("<b>StrategyRegistry</b><br/>codeHash · datasetHash<br/>OOS metrics · origin · status")]
        guard[("<b>PolicyGuard</b><br/>venue allowlist · balance cap<br/>session TTL · kill switch")]
        wallet["<b>Per-strategy wallet</b><br/>bound to the hash<br/>capped at $100 / 24h"]
        venues["<b>Venues</b><br/>SpotVenue stub · StableFX RFQ"]
        nano[("<b>Nanopayments</b><br/>batched micro-USDC<br/>one event per call")]
    end

    subgraph meter["The meter — everything above pays it"]
        x402["<b>x402 gateway</b><br/>inference $0.002 · data $0.0005<br/>backtest $0.05"]
        ledger["<b>Cost of Intelligence</b><br/>spend vs live P&L"]
    end

    forge --> validate
    user --> validate
    validate -->|rejected| mortem["<b>Post-mortem</b><br/>fed back to the Forge"]
    validate --> sandbox --> backtest --> hurdle
    hurdle -->|no| mortem
    hurdle -->|yes| paper
    paper -->|diverges| mortem
    paper -->|confirms| registry
    registry --> wallet
    guard -.->|authorises every trade| wallet
    wallet --> venues
    venues -->|breach drawdown| demote["<b>StrategyDemoted</b><br/>reason published · wallet swept"]
    demote --> mortem
    mortem -.->|negative evidence| forge

    x402 -.-> forge
    x402 -.-> backtest
    x402 -.-> user
    x402 --> nano --> ledger
Loading

What lives where

Path What it is
packages/strategy-runtime Python, stdlib only. The Strategy interface, AST validator, rlimit-jailed sandbox, walk-forward backtester, cost models, paper arena, template library. Exposed over HTTP — that boundary is also the metered surface.
packages/contracts Foundry. StrategyRegistry, PolicyGuard, SpotVenue, Nanopayments, plus decimals-faithful test tokens for local runs.
apps/agent Node/TS. Forge, evaluation gate, graduator, live harness, x402 meter, venue routers, REST + WebSocket API, SQLite population DB.
apps/web Next.js 14. Factory floor, strategy pages, submit flow, leaderboard, Cost of Intelligence ledger.
scripts demo, demo:submit, demo:demote, demo:security, deploy, seed.

The Strategy interface

Everything is built around this one contract. A strategy is pure decision logic — the harness owns all I/O, venue routing, sizing limits and signing. That separation is what makes LLM generation reliable, user submission safe, and sandboxing tractable.

class Strategy:
    params: dict          # tunables, with declared param_ranges
    universe: list[str]   # ["USDC/EURC", "ETH/USD"]
    hypothesis: str       # what effect do you claim exists?

    def on_tick(self, market: MarketState) -> list[Order]: ...
    def on_fill(self, fill: Fill) -> None: ...

MarketState carries prices, RFQ quotes, positions, cash, equity and a set of strictly causal features (zscore_20, rsi_14, vol_60, …). A strategy cannot import anything, cannot reach the network or disk, and cannot name a venue.


Containment: three layers, and the third is the one that counts

Generated code and user code are treated as exactly as untrusted as each other. One security model, no privileged path.

Layer Contains Demonstrated by
1 AST allowlist — no imports, no eval/exec/open/getattr, no dunder attribute access, no try/with/lambda, bounded loops and complexity careless and casual malice, cheaply, before execution pnpm demo:security
2 Sandboxed subprocessRLIMIT_CPU, RLIMIT_AS, RLIMIT_FSIZE=0, RLIMIT_NPROC, own process group, wall-clock kill, scrubbed __builtins__, poisoned sys.modules anything that compiles — CPU bombs, allocation bombs, novel escapes pnpm demo:security
3 PolicyGuard, on-chain — venue allowlist, cumulative balance cap, session TTL, global kill switch capital pnpm demo:security, forge test

Layers 1 and 2 contain code. Layer 3 contains money — it is the only guarantee that survives an escape nobody anticipated, because the guard is what authorises the trade, and it reverts.


Verifiable graduation

No strategy touches live capital unless, in this order:

  1. keccak256(source) + dataset hash + OOS metrics are written to StrategyRegistry;
  2. a dedicated wallet is provisioned and its PolicyGuard policy is set;
  3. only then does the registry mark the strategy Live and bind the wallet.

There is never an instant where a funded wallet exists without a policy binding it. No record → no wallet → no capital, structurally rather than procedurally.

Verify any published strategy yourself:

curl -s localhost:8787/api/verify -H 'content-type: application/json' \
  -d '{"source": "<the exact module>", "datasetHash": "0x…"}'
# → { registered, datasetMatches, status, oosSharpe }

Per-strategy P&L is that wallet's on-chain history. Not a number we compute and ask you to believe.


Backtests are honest, and say so

  • Walk-forward, out-of-sample only. Train windows are never scored; the reported equity curve is the concatenation of test segments.
  • No lookahead, structurally. Features at bar i are computed from bars 0..i; an order emitted at bar i fills at bar i+1. Both are asserted by tests that scramble future data and require the report to be byte-identical.
  • Costs charged in full. StableFX-calibrated spread, square-root market impact, and gas — which on Arc is denominated in USDC and therefore exactly modellable rather than hand-waved.
  • Overfitting is penalised. fitness = OOS_Sharpe − complexity_penalty − instability_penalty, where instability is the spread of per-window Sharpes.
  • The hurdle is read from the chain. A strategy must beat live USYC APY plus its own modelled costs, and stay inside the drawdown mandate.
  • Every report is labelled SIMULATED — not indicative of future returns and stamped with the dataset hash.

On the data. With no network, the dataset generator produces deterministic synthetic series carrying documented structure (Ornstein-Uhlenbeck reversion in USDC/EURC, AR(1) momentum in ETH/USD). This is deliberate and disclosed: a pure random walk has no edge, so every strategy would score zero minus costs and the factory would be selecting on noise — the pipeline would run but prove nothing. Costs, out-of-sample discipline and the hurdle all still apply, and badly parameterised strategies still fail. Set a reachable PYTH_HERMES_URL and real Pyth history is used instead.


Demo script (5 minutes)

pnpm install
pnpm demo                 # everything, from nothing

1 · The floorhttp://localhost:3000

Candidates flow left to right: intake → backtesting → paper arena → live → post-mortem. The event feed narrates every decision. Note the rejected column: each entry carries why, and the Forge reads those before its next generation.

2 · A strategy that made it — click any card in Live

Hypothesis, the exact module that was hashed, the walk-forward report with per-window results, the registry record, the policy-capped wallet, and its fills with transaction hashes.

3 · Submit your ownhttp://localhost:3000/submit

pnpm demo:submit          # or use the UI

Type a sentence. Watch the read-back — it describes what the generated code actually does, derived from the code rather than echoed from your description. That step is the point: it catches a misunderstanding before you pay for a backtest and long before you start trusting its numbers. Then pay $0.05 through the same meter the agent pays.

4 · The economicshttp://localhost:3000/ledger

Cost of Intelligence against live P&L, itemised per call, with real on-chain Nanopayments settlements.

5 · Containment

pnpm demo:security        # all three layers, proven
pnpm demo:demote          # a live strategy breaches its mandate

The demotion publishes StrategyDemoted(codeHash, reason) with the reason string intact, revokes and sweeps the wallet, and writes a post-mortem back into the population. A registry that kept only its winners would be exactly the selective disclosure this project exists to argue against.


Environment

Every variable is optional. With an empty .env the whole thing runs. See .env.example for the full annotated list.

Variable Default What changes when you set it
CHAIN_MODE anvil arc targets Arc testnet and the real USDC/EURC/USYC predeploys
AGENT_PRIVATE_KEY anvil account #0 Required for CHAIN_MODE=arc. Must hold testnet USDC
ANTHROPIC_API_KEY Forge switches from the template library to real Claude generation, mutation, crossover, plain-English compilation and LLM read-backs
CIRCLE_API_KEY Per-strategy wallets become developer-controlled Circle SCAs instead of derived local keys
STABLEFX_API_KEY FX leg uses real StableFX RFQ instead of the local market maker
PYTH_HERMES_URL Hermes Real Pyth history and live prices; unreachable → synthetic, labelled
PAPER_SESSION_MINUTES 2 Paper confirmation length. Production would be days
MAX_DRAWDOWN_BPS 1200 Mandate cap; breach ⇒ demotion
STRATEGY_WALLET_CAP_USDC 100 PolicyGuard balance cap per strategy
MAX_LIVE_STRATEGIES 8 Live concurrency. A legibility limit — PolicyGuard is the safety one
PROMOTION_MIN_SHARPE 0.60 Floor, on top of the on-chain USYC hurdle
NANOPAY_SETTLE_BATCH 5 IOUs per settlement transaction

The app never crashes on a missing optional key; it degrades to the mock and says so at boot and in the UI.

Going real

# Arc testnet
CHAIN_MODE=arc
AGENT_PRIVATE_KEY=0x…            # fund from the Arc faucet
pnpm deploy:contracts

# Circle developer-controlled wallets
CIRCLE_API_KEY=…
CIRCLE_ENTITY_SECRET=…
CIRCLE_WALLET_SET_ID=…
pnpm add -w @circle-fin/developer-controlled-wallets   # optional peer

# Live LLM Forge
ANTHROPIC_API_KEY=sk-ant-…

# StableFX — requires KYB approval; the mock RFQ implements the same interface
STABLEFX_API_KEY=…

Arc addresses: USDC 0x3600000000000000000000000000000000000000 · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a · USYC 0xe9185F0c5F296Ed1797AaE4238D26CCaBEadb86C · FxEscrow 0x867650F5eAe8df91445971f14d89fd84F0C9a9f8 · Permit2 0x000000000022D473030F116dDEE9F6B43aC78BA3 · explorer https://testnet.arcscan.app

The decimals trap

Arc's native gas asset is USDC with 18 decimals, while the USDC ERC-20 interface at 0x3600…0000 reports 6. Both are called "USDC" and they are 1e12 apart. A single hardcoded 1e18 in the token path is a trillion-fold error, and it is silent, because every number involved still looks plausible.

Nothing in Nectar hardcodes a scale factor — decimals() is read at boot and everything scales from it. packages/contracts/test/Decimals.t.sol and apps/agent/src/lib/decimals.test.ts both assert the discipline, and the local test tokens take decimals as a constructor argument specifically so anvil runs reproduce the trap rather than hiding it.


Tests

pnpm test              # everything
pnpm test:contracts    # forge — registry, PolicyGuard, decimals
pnpm test:py           # pytest — validator, sandbox, walk-forward, no-lookahead
pnpm test:agent        # vitest — decimals, diversity guard, promotion gate

The tests worth reading, because they encode the claims:

  • test_backtest.py::test_report_unchanged_when_future_bars_are_scrambled — no lookahead, end to end.
  • test_backtest.py::test_signal_fills_at_next_bar_not_current — a strategy cannot trade a price it has already seen.
  • test_sandbox.py::test_cpu_bomb_is_killed_by_rlimit — containment past the validator.
  • PolicyGuard.t.sol::test_non_allowlisted_venue_is_blocked — the chain refusing to sign.
  • StrategyRegistry.t.sol::test_cannot_bind_wallet_without_registration — no record, no capital.
  • Decimals.t.sol::test_one_dollar_differs_by_1e12_between_representations — the trap, pinned.
  • diversity.test.ts — why a parametric variant is admitted and a duplicate is not.

Honest limitations

  • Venue liquidity is synthetic. SpotVenue is a constant-product stub with operator-seeded reserves, labelled SIMULATED everywhere it surfaces. What is real is the chain from code hash → registry → policy-capped wallet → signed, settled receipt. Dressing up the depth would undercut the only part worth trusting.
  • No user funds, ever. Users buy a backtest of their own strategy — a software service. Live execution trades only the operator's testnet capital. "Run your strategy with your own wallet" needs a legal wrapper this build does not have.
  • The mock RFQ has no counterparty. It quotes, widens with size, signs and expires like the real thing, and the backtester is calibrated against the same spreads — but nobody is on the other side.
  • Nothing here promises profitability. The product is the pipeline and its verifiability. A registry record is a proof of process, not of performance.

Design decisions and the reasoning behind them, including the ones that deviate from the original plan, are in DECISIONS.md.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages