Skip to main content
ClaudeWave
Skill510 repo starsupdated today

CTRL

CTRL is a Base mainnet automation protocol that compiles natural-language workflow descriptions into on-chain trigger-action vaults. Users sign once via EIP-5792 batch to authorize recurring or event-driven actions (DCA, price-gated swaps, launchpad sniping, whale-following) with pre-set per-swap and daily spend caps, after which a Render-hosted keeper executes autonomously every five seconds. Use it to automate crypto trading strategies without repeated wallet interactions.

Install in Claude Code
Copy
git clone --depth 1 https://github.com/aaronjmars/aeon /tmp/ctrl && cp -r /tmp/ctrl/skills/ctrl ~/.claude/skills/ctrl
Then start a new Claude Code session; the skill loads automatically.

SKILL.md

> **${var}** — Natural-language description of the workflow to build, e.g. `DCA 0.005 ETH into USDC every week`. Required. If empty, log `CTRL_NO_INTENT` and exit cleanly (no notify).

CTRL runs on **Base mainnet** only. It compiles a `trigger → action` graph into a V3 vault, the wallet signs **once** — an EIP-5792 batch deploys the vault and registers spending rules — and a Render-hosted keeper polls every ~5s and executes from there. The security boundary is the user's signature at activate-time, not an API key at create-time, so the create + read REST endpoints are anonymous. Activation requires a real wallet session, which the user provides by opening the CTRL activate landing page in their browser.

Read the last 2 days of `memory/logs/` so a re-run can reference an existing workflow id instead of provisioning a new one.

## Config

- API root = `https://ctrl.build/api/mcp`. No key required for the endpoints used here.
- Chain = Base mainnet (chainId `8453`). Always send `"chain": "base"` and `"network": "mainnet"` — CTRL does NOT support Ethereum, Arbitrum, Solana, or any other chain. If the user asks for another chain, log `CTRL_CHAIN_UNSUPPORTED` and exit before creating anything.
- Sensible defaults the agent should adopt if the user does not state them: per-swap ≤ `0.01 ETH`, per-day ≤ `0.1 ETH`, slippage ≤ `1%` for stable-pair DCA, `15%` only for launchpad sniping. Caps are signed at activate-time, never embedded in the workflow body.

## Steps

### 1. Read the live block catalog

CTRL exposes 24 blocks across four categories (triggers, actions, conditions, utilities). Every key under each node's `data.config` MUST match a `fields[].key` in the catalog — invented keys are silently dropped by the keeper.

```bash
curl -m 10 -s "https://ctrl.build/api/mcp/block-catalog" \
  | jq '{
      triggers:   [.triggers[].id],
      actions:    [.actions[].id],
      conditions: [.conditions[].id],
      utilities:  [.utilities[].id]
    }'
```

Pick the trigger + action ids that match `${var}`. The most common shapes:

- Recurring schedule → `time.interval` (config: `minutes`). The only schedule trigger today — express weekly as `minutes: 10080`, daily as `1440`, hourly as `60`. There is no cron / day-of-week trigger yet.
- Price gate → `price.above` / `price.below` (config: `token`, `threshold`)
- New token launch → `pool.created` (config: `launchpad`, `safetyEnabled: true` for GoPlus honeypot/tax/score gating)
- Swap action → `cypher.swap` (config: `tokenIn`, `tokenOut`, `amount`, `slippage`)
- Telegram alert → `notify.telegram` (config: `message`, `severity`)

If nothing in the catalog matches the intent, log `CTRL_NO_BLOCK_MATCH` and notify the user that the intent is not supported yet.

### 2. Compose the workflow graph

The workflow is a ReactFlow-style `{ nodes, edges }` graph. Each node carries BOTH a top-level `blockType + blockSubtype` (for the create-time validator) AND a `data.blockId + data.subtype` (for the keeper). The two are redundant by design — get them all from the catalog row.

Minimum viable weekly DCA — buy 0.005 ETH of USDC every 10080 minutes, 1% slippage. Write it to `body.json` (step 3 reads that file):

```bash
cat > body.json <<'JSON'
{
  "name": "Weekly DCA — ETH to USDC",
  "description": "DCA 0.005 ETH into USDC weekly via CTRL",
  "chain": "base",
  "network": "mainnet",
  "workflow_data": {
    "nodes": [
      {
        "id": "t1",
        "type": "trigger",
        "blockType": "trigger",
        "blockSubtype": "interval",
        "position": { "x": 200, "y": 200 },
        "data": {
          "blockId": "time.interval",
          "subtype": "interval",
          "label": "Every week",
          "config": { "minutes": 10080 }
        }
      },
      {
        "id": "a1",
        "type": "action",
        "blockType": "action",
        "blockSubtype": "swap",
        "position": { "x": 500, "y": 200 },
        "data": {
          "blockId": "cypher.swap",
          "subtype": "swap",
          "label": "Buy USDC",
          "config": {
            "tokenIn": "ETH",
            "tokenOut": "USDC",
            "amount": 0.005,
            "slippage": 1,
            "useNativeETH": true
          }
        }
      }
    ],
    "edges": [
      { "id": "e1", "source": "t1", "target": "a1" }
    ]
  }
}
JSON
```

Units the catalog is explicit about:

- `amount` is in **token units** (e.g. `0.005` ETH when `tokenIn` = `ETH`). Beta cap: ≤ 1 ETH-equivalent per swap.
- `slippage` is **percent**, range `0.1–99`. Snipe flows force ≥ 10. For stable-pair DCA pick 0.5–2; for memecoin snipes 10–15.
- The trigger's `minutes` is an integer ≥ 1.

### 3. Create the workflow (draft, anonymous)

```bash
WORKFLOW=$(curl -m 15 -s -X POST "https://ctrl.build/api/mcp/workflows" \
  -H "Content-Type: application/json" -d @body.json)
WID=$(printf '%s' "$WORKFLOW" | jq -r '.workflow.id')
[ -n "$WID" ] && [ "$WID" != "null" ] || { echo "CTRL_CREATE_FAILED"; exit 1; }
```

The response is `{ "workflow": { "id", "name", "status", "created_at" } }`. The draft's `user_id` stays NULL until the wallet that signs the activation batch claims it. Drafts that are never activated auto-prune.

### 4. Optional pre-flight — check vault state

`${USER_WALLET}` below is the operator's connected wallet address (the vault owner, a `0x…` address) — read it from `memory/` or ask the user once. Before sending them to sign, surface the vault preview so they know whether a deploy is included and how much ETH to fund:

```bash
USER_WALLET="0x..."   # the operator's wallet (vault owner)
curl -s "https://ctrl.build/api/mcp/vault-status?wallet=${USER_WALLET}" | jq '{
  vaultExists, vaultAddress, predictedVaultAddress,
  ethBalance: .balances.ethDecimal,
  wethBalance: .balances.wethDecimal,
  ready, warnings
}'
```

If `vaultExists` is `false`, the activate batch will deploy the V3 vault in the same transaction list — fine, but the user must fund it (the user picks `depositEth` on the activate page).

###