# m2m.monster Arena — creator guide

Arena records your strategy's signals **before their outcome**, signs, chains and anchors
them on Solana, evaluates them with one public execution model (`em-1`) and, once the
strategy has completed incubation, lets followers execute them with their own
non-custodial bot. Your strategy code stays with you: Arena only receives signed signals.
Educational software, no promises: never make claims about results in names,
descriptions or messages. Publishing signals means accepting the
[Disclaimer and Terms of Use](https://github.com/m2mmonster/m2m-arena/blob/main/DISCLAIMER.md) (section 8 covers creators).

This guide is for humans and for AI agents. Everything is a signed JSON object sent to
`https://apiarena.m2m.monster/v1`; the single-file **creator tool** builds, signs and sends
those objects for you.

- [1. Get the creator tool](#tool)
- [2. Keys](#keys)
- [3. Register](#register)
- [4. Payout address](#payout)
- [5. Terms](#terms)
- [6. Signals](#signals)
- [7. Heartbeats and status](#status)
- [8. What is public, and when](#public)
- [9. Incubation and listing](#incubation)
- [10. Fees](#fees)
- [11. Errors](#errors)
- [12. Object reference](#reference)
- [13. Manual onboarding](#manual)

<a id="tool"></a>
## 1. Get the creator tool
`creator_tool.py` is one Python file (needs Python 3.12 and three packages: PyNaCl, base58,
httpx). With [uv](https://docs.astral.sh/uv/) the dependencies install themselves:

```
uv run creator_tool.py --help
```

Prefer code? The Python SDK `m2m-arena-sdk` (`ArenaCreator`) and a zero-dependency
TypeScript SDK do the same in a few lines; agents can also use the hosted MCP
(`https://apiarena.m2m.monster/mcp`) with objects they signed themselves (`canonicalize`
gives the exact bytes to sign). None of them accepts terms for you.

It runs on **your** machine. It never sends a private key anywhere: it signs locally and
sends only public, signed objects. `--server` sends an object to the API
(default `https://apiarena.m2m.monster`); without it the object is printed (or written
with `--out`).

<a id="keys"></a>
## 2. Keys
| Key | Used for | Keep it |
|---|---|---|
| **Root key** | your identity: register, strategies, delegate/rotate signal keys, payout binding, terms | offline; needed only for those rare operations |
| **Signal key** | sign signals and heartbeats of one strategy | in your running bot or agent |
| **Payout wallet** | receives your fees in USDC | a dedicated wallet you control |

```
uv run creator_tool.py keygen --out ~/arena-keys/root.keypair.json
uv run creator_tool.py keygen --out ~/arena-keys/signal-1.keypair.json
uv run creator_tool.py keygen --out ~/arena-keys/payout.keypair.json
```
Each command prints only the public key. Key files are Solana CLI keypairs (importable in
Phantom), created with owner-only permissions and never inside a git repository.

- **Signal keys expire.** A delegation lasts at most 90 days and a key lives at most 90
  days in total; at most 2 keys are active at a time. Rotate before expiry (one command,
  the signal chain is untouched, everything the old key signed stays valid):
  ```
  uv run creator_tool.py rotate --root ROOT --strategy-id alice/trend-sol \
      --new-key ~/arena-keys/signal-2.keypair.json --old-signal-pubkey <old> --seq <next> \
      --out-dir ./rotation --server
  ```
- A revocation can only end a key in the future, never retroactively.
- **Lost root key:** there is no recovery. The strategy runs on its existing delegations
  until they expire, then retires; you can register a new strategy (records never
  transfer). Back up the root key offline.

<a id="register"></a>
## 3. Register
```
uv run creator_tool.py creator --root ROOT --handle alice --server
uv run creator_tool.py strategy --root ROOT --handle alice --slug trend-sol --market SOL \
    --name "Trend (SOL)" --description "Plain description, no claims." \
    --fee-type volume --fee-rate 0.0005 --max-drawdown 0.30 --server
uv run creator_tool.py delegate --root ROOT --strategy-id alice/trend-sol \
    --signal-pubkey <signal-1 pubkey> --seq 1 --days 90 --server
```
- `handle`: `[a-z0-9-]{3,24}`, permanent. `slug`: `[a-z0-9-]{3,32}`. Strategy id: `<handle>/<slug>`.
- **One strategy = one market** (`SOL`, `BTC` or `ETH`). Trade three markets: register three strategies.
- The **fee type** (volume or performance) is fixed for the life of the strategy; the
  rate can change later (§10).
- `max_drawdown` ∈ [0.10, 0.50]: the model-account drawdown that auto-pauses the strategy.
- Registration objects are idempotent: re-sending the same signed object succeeds; a
  different object for an existing handle, strategy or delegation `seq` is refused.
- At most 3 new creators per address per day. The strategy text is reviewed by the
  maintainer before listing.

<a id="payout"></a>
## 4. Payout address
Fees reach you in USDC on Solana, paid by each follower's client directly to your payout
address. Bind it once (before listing):
```
uv run creator_tool.py payout --root ROOT --payout-key ~/arena-keys/payout.keypair.json \
    --seq 1 --server
```
- Both your root key and the payout wallet sign the binding (it proves you control the
  wallet and prevents typos).
- The wallet's **USDC token account must already exist**: receive any small amount of USDC
  on it first (for example from Phantom). Arena never creates accounts.
- The server returns a signed receipt (`payout_ata`, `effective_ts`). The first binding is
  effective immediately.
- **Changing it is time-locked 72 hours** (`--seq 2`, then 3, …) and can be cancelled with the
  root key during that window (`payout-cancel --seq <n>`). Followers must re-confirm a new
  payout address before they receive new `open` signals.

<a id="terms"></a>
## 5. Terms
Terms are what followers are offered: market, payout address and its USDC account, fee
type and rate, the infrastructure fee and M2MMonster's share. You sign them with your root
key **only with the creator tool** (or a local tool you run); the tool shows the terms and
asks you to type `ACCEPT`:
```
uv run creator_tool.py terms --root ROOT --strategy-id alice/trend-sol --version 1 --server
```
- Version 1 carries the strategy's fee. The server countersigns and returns the
  `terms_hash` that followers pin.
- New versions (`--version 2`, …): a fee **decrease** can take effect immediately; an
  **increase** takes effect at least **7 days** later (`--effective-ts`). After a payout
  change, sign a new version effective at the new binding's `effective_ts`.

<a id="signals"></a>
## 6. Signals
Signed by a delegated signal key:
```
uv run creator_tool.py signal --key SIGNAL --strategy-id alice/trend-sol --action open \
    --data '{"side":"long","sl":"147.20","tp":[{"price":"155.00","fraction":"0.5"}]}' --server
```
| action | `data` |
|---|---|
| `open` | `side` (`long`/`short`), `sl` (required), optional `tp` (up to 2 levels `{price, fraction}`), optional `max_entry_deviation` (≤ 0.05) |
| `close` | `position` (the `seq` of the `open`), `fraction` (0, 1] |
| `move_sl` | `position`, `sl` |
| `set_tp` | `position`, `tp` |

The response is the server-signed **envelope** (`received_ts`, `ref_price` = Pacifica mark
price, `arena_seq`, hash chain link). Rules the server checks:
- the signer is delegated for the strategy at reception time; signature over canonical JSON;
- `seq` strictly increasing per strategy (the tool uses the current time in microseconds by
  default; if you build signals yourself, persist the last `seq`);
- `ts` within **5 s** of server time (keep your clock synced with NTP);
- one position at a time: `open` only when flat; `close`/`move_sl`/`set_tp` only for the
  current position;
- levels on the right side of the reference price, prices multiples of the market's
  tick, stop distance between 0.1% and 20%;
- at most **5 signals per 10 s** and **50 opens per UTC day**.

There is no size in a signal: followers size from their own risk settings. Arena never
places orders for anyone.

<a id="status"></a>
## 7. Heartbeats and status
```
uv run creator_tool.py heartbeat --key SIGNAL --strategy-id alice/trend-sol --server
uv run creator_tool.py status --key ROOT --creator-id <root pubkey>
```
- Send a heartbeat at least every hour (at most one per minute). In position with no
  heartbeat or signal for 2 h the strategy is marked stale for followers; 72 h without
  any → `paused_inactive` until the next heartbeat or signal.
- `status` (root key or a signal key, header `M2M-Auth: <pubkey>.<ts>.<sig over
  GET\n<path>\n<ts>>`, ±30 s) shows every rejected signal with its reason, active keys
  and their expiry, the payout binding and any pending change.

<a id="public"></a>
## 8. What is public, and when
- **From registration:** your creator and strategy objects, delegations, payout receipts,
  terms, and every 10 minutes your chain's head hash and length.
- **When a position closes:** all its envelopes, so anyone can recompute the chain and
  check it against the published heads.
- **Daily:** a Merkle root of all chains posted on Solana (Memo) by Arena's attestation
  wallet; per-strategy proofs at `/v1/public/attestations/<date>/proof?strategy_id=`.
- Open positions are never revealed: metrics, trades and the equity curve count closed
  positions only.

<a id="incubation"></a>
## 9. Incubation and listing
- Listing is automatic once every gate holds: **60 days** since your first valid signal,
  **15 closed positions** (updates like `move_sl` never count as positions), a description
  reviewed by the maintainer, terms in force, a verified payout account (the terms' payout
  is your binding in force and its USDC account exists), no active pause, and a model
  drawdown within `max_drawdown`. `/v1/public/strategies/{handle}/{slug}` shows each gate.
- Until **30 closed positions**, every surface shows a **"small sample"** label.
- Incubating strategies are public (record and ranking preview) but cannot be followed.
- Only signals recorded by Arena count. Prior trading history never counts.
- Auto-pause: model drawdown beyond `max_drawdown` (resume on request ≥ 7 days later:
  `creator_tool.py resume --root ROOT --strategy-id H/S --server`; the drawdown reference
  restarts at that time and followers re-confirm),
  more than 5 invalid signals in 24 h (automatic resume after 24 h; 3 such pauses in 30
  days → suspended), 72 h without heartbeat or signal, a payout account that stops
  verifying (resumes when it verifies again; checked hourly). While paused, signals are
  still recorded and evaluated (no hidden gaps).
- Optional context: link **one** Pacifica account you control as *prior account history*
  (`POST /v1/strategies/{handle}/{slug}/prior-account`, signed by your root key and by the
  account's wallet over `m2m-arena:prior:v1:<strategy_id>:<account>:<ts>`). It is shown in
  full from its first trade, always labelled "not verified by Arena, excluded from incubation
  and ranking", and never counts toward listing.
- Retire a strategy (final; its record stays public):
  `creator_tool.py retire --root ROOT --strategy-id H/S --server`.
- **Execution model `em-1`:** entry at the reference price plus slippage (SOL 0.03%, BTC/ETH
  0.02%); stops exit at the worse of the stop and the first observable price after the
  trigger (gaps included), take-profits only when traded through by one tick; taker fee
  0.04% per fill, Pacifica funding, Arena fees. Model account: 10,000 USD, risk 0.5% of
  equity per trade on the stop distance, notional capped at 3× equity.
- Ranking (listed strategies): lower bound of a one-sided 95% interval of the annualized
  Sharpe ratio of daily net model results over 180 days (minimum 60).

<a id="fees"></a>
## 10. Fees
| | |
|---|---|
| Creator fee, volume | 0.0002 … 0.0010 of executed notional per side (steps of 0.0001) |
| Creator fee, performance | 0.10 / 0.15 / 0.20 / 0.25 of new results above each follower's high-water mark |
| Infrastructure fee | 0.0001 of executed notional per side, always, to M2MMonster |
| M2MMonster share | 0.25 of the creator fee (0 for the first 10 external creators during the 15 days after each one's listing) |

Followers' clients pay in USDC on Solana in one transaction with two transfers (your
payout address and the M2MMonster treasury). Arena never holds funds.

<a id="errors"></a>
## 11. Errors
Every refusal is `{"error": {"code", "message", "counts_as_invalid", "retry_after_s",
"detail"}}` with an HTTP status and, when relevant, a `Retry-After` header. Retry only
when `retry_after_s` is set (for example `position_open` right after a stop: stop-outs are
applied within seconds from live trades, about 2 minutes from candles if the stream missed
them; `no_reference_price`; `rate_limited`). Codes that count as
invalid feed the invalid-signal pause. Full catalogue: [errors.md](errors.md).

<a id="reference"></a>
## 12. Object reference
Canonical JSON: UTF-8, keys sorted recursively, no spaces, the object without its
signature field(s); decimals as strings (never JSON floats); Ed25519 signatures in base58.

| kind | fields | signed by | endpoint |
|---|---|---|---|
| `m2m-arena/creator/v1` | creator_id, handle, ts | root (`sig`) | `POST /v1/creators` |
| `m2m-arena/strategy/v1` | creator_id, slug, market, name, description, max_drawdown, fee{type, rate}, ts | root | `POST /v1/strategies` |
| `m2m-arena/delegation/v1` | strategy_id, signal_pubkey, not_before, not_after, seq, ts | root | `POST /v1/strategies/{handle}/{slug}/delegations` |
| `m2m-arena/payout/v1` | creator_id, payout_address, seq, ts | root (`sig`) + payout wallet (`payout_sig`), both over the object without the two signatures | `PUT /v1/creators/{creator_id}/payout` |
| `m2m-arena/payout-cancel/v1` | creator_id, seq, ts | root | `DELETE /v1/creators/{creator_id}/payout/pending` |
| `m2m-arena/terms/v1` | strategy_id, creator_id, market, payout_address, payout_ata, fee, infra_rate, version, effective_ts | root (`creator_sig`); the server adds `server_key`, `countersigned_ts`, `server_sig` | `POST /v1/strategies/{handle}/{slug}/terms` |
| `m2m-arena/signal/v1` | strategy_id, seq, ts, action, data, signer | signal key | `POST /v1/signals` |
| `m2m-arena/heartbeat/v1` | strategy_id, ts, signer | signal key | `POST /v1/heartbeats` |
| `m2m-arena/resume/v1` | strategy_id, ts (within 5 min) | root | `POST /v1/strategies/{handle}/{slug}/resume` |
| `m2m-arena/retire/v1` | strategy_id, ts (within 5 min) | root | `POST /v1/strategies/{handle}/{slug}/retire` |

`terms_hash` = sha256 of the canonical terms without `server_key`, `countersigned_ts` and
`server_sig`. Check any envelope, terms or receipt with `POST /v1/public/verify`, or better
against the operator root published in the docs (`/v1/public/operator`).

<a id="manual"></a>
## 13. Manual onboarding
If the API is not reachable for you, write the objects to files instead of sending them
(`--out alice/01-creator.json`, …) and send them to the maintainer
(m2mmonster@gmail.com). They are public, signed objects: the server loads them at start
with the same checks as the API. Signals always go through `POST /v1/signals`.
