ctrl

star 517

Build on-chain automation workflows on Base via CTRL. Use for recurring or triggered actions — DCA, price-gated swaps, launchpad sniping, whale-follow — that should run autonomously after a single wallet signature. The wallet signs once (EIP-5792 batch), and the CTRL keeper executes every trigger after, bounded by per-swap and per-day caps the user pre-authorized.

aaronjmars By aaronjmars schedule Updated 6/15/2026

name: CTRL category: crypto description: Build on-chain automation workflows on Base via CTRL. Use for recurring or triggered actions — DCA, price-gated swaps, launchpad sniping, whale-follow — that should run autonomously after a single wallet signature. The wallet signs once (EIP-5792 batch), and the CTRL keeper executes every trigger after, bounded by per-swap and per-day caps the user pre-authorized. var: "" tags: [crypto, automation, base, defi] capabilities: [external_api, writes_external_host, onchain_writes, sends_notifications]

${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.

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):

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)

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:

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).

5. Hand the user the activate landing page

CTRL's activate endpoint (POST /api/mcp/activate/<id>) requires wallet auth to encode the EIP-5792 batch — the agent cannot call it from a server sandbox. Instead, construct the landing-page URL and notify the user. The landing page handles wallet-connect, lets the user adjust caps, calls activate from their authenticated session, then hands the returned transactions[] to their wallet's EIP-5792 wallet_sendCalls flow.

ACTIVATE_URL="https://ctrl.build/activate/${WID}"

Send via ./notify. Keep it under 4000 chars and put the URL last so it stays clickable:

*CTRL workflow drafted — ready to sign*
${var}

Chain: Base mainnet
Suggested caps: 0.01 ETH/swap · 0.1 ETH/day (editable on the activate page)
One wallet signature deploys the vault + registers the spending rule. The keeper runs every trigger after that.

${ACTIVATE_URL}

The user opens the URL, connects a wallet, optionally edits maxPerSwapEth / maxPerDayEth / depositEth / expiryDays, and signs ONE EIP-5792 batch. From that moment the keeper takes over.

6. Log

Append to memory/logs/${today}.md:

## ctrl
- Intent: ${var}
- Workflow id: ${WID}
- Chain: base / mainnet
- Trigger: time.interval / price.above / pool.created / ...
- Action: cypher.swap → USDC
- Activate URL sent: ${ACTIVATE_URL}
- Status: pending-signature

End-states: CTRL_OK, CTRL_NO_INTENT, CTRL_NO_BLOCK_MATCH, CTRL_CREATE_FAILED, CTRL_CHAIN_UNSUPPORTED, CTRL_REJECTED_ADVICE.

Follow-up reads

Once a workflow is live, both endpoints work anonymously by id / wallet:

# Vault address, balances, active spending rules, ready flag.
curl -s "https://ctrl.build/api/mcp/vault-status?wallet=${USER_WALLET}" | jq '.activeRules'

# Last N executions for this workflow — surfaces tx hashes, error messages, gas spent.
curl -s "https://ctrl.build/api/mcp/execution-logs?workflow_id=${WID}" | jq '.logs[0:5]'

Use these to write a one-line health line in a follow-up notify ("3 executions in last 24h, last trigger 2h ago, vault balance 0.034 ETH").

Withdrawing from the vault

To pull funds back out of the vault, POST /api/mcp/vault-withdraw. Like activate, the agent never signs — it returns an EIP-5792 transactions[] batch + a signUrl the user opens in their wallet. token defaults to ETH; pass WETH or a 0x token address for other assets; omit amount to withdraw the full balance.

curl -s -X POST "https://ctrl.build/api/mcp/vault-withdraw" \
  -H "Content-Type: application/json" \
  -d "{ \"wallet\": \"${USER_WALLET}\", \"token\": \"ETH\", \"amount\": \"0.05\" }" \
  | jq '{ vaultAddress, amount, signUrl }'

Notify the user with the returned signUrl — they sign one transaction and the funds land back in their wallet.

Surfaces

This skill uses CTRL's anonymous REST API (/api/mcp/*, no key) — the canonical surface for wallet-native agents, since the wallet signature at activate-time is the trust boundary. CTRL also runs a key-gated JSON-RPC MCP server at the same base for desktop clients (Cursor, Claude Desktop) where tool execution needs an sk_ctrl_ key; both drive identical workflows. For an aeon skill, stick with REST.

Sandbox note

/api/mcp/block-catalog, /api/mcp/workflows (POST), /api/mcp/vault-status, and /api/mcp/execution-logs are public over HTTPS with no auth headers. /api/mcp/activate/<id> is NOT public — never try to call it from the agent; it requires a wallet-authenticated session and is what the landing page invokes for the user.

Constraints

  • Never auto-sign. The wallet popup on the activate landing page is the trust boundary; the agent only hands the user the URL.
  • Never embed caps in the workflow body. vaultCaps is not a workflow field — caps are set when the user signs activate (maxPerSwapEth, maxPerDayEth, decimal strings).
  • Match slippage to the use case. Catalog default is 15 (= 15%) which is correct for sniping and disastrous for stable-pair DCA. For DCA / ETH↔USDC use 0.5–2. For memecoin snipes 10–15.
  • No trade advice. If the intent is "buy X because it will moon", refuse and log CTRL_REJECTED_ADVICE. CTRL is execution infrastructure, not a signal source.
  • One workflow per invocation. Multi-step strategies → chain blocks inside one workflow (trigger → swap → notify), not multiple skill runs.
  • Base only. Always set chain: "base", network: "mainnet". Any other chain → log CTRL_CHAIN_UNSUPPORTED and exit. Note: the create endpoint will accept other chain strings (it does not validate), so the agent itself is the gate.

Resources

Install via CLI
npx skills add https://github.com/aaronjmars/aeon --skill ctrl
Repository Details
star Stars 517
call_split Forks 176
navigation Branch main
article Path SKILL.md
More from Creator