Skip to content

skills: add CTRL — on-chain automation on Base#353

Merged
aaronjmars merged 3 commits into
aaronjmars:mainfrom
daxaur:add-ctrl-skill
Jun 12, 2026
Merged

skills: add CTRL — on-chain automation on Base#353
aaronjmars merged 3 commits into
aaronjmars:mainfrom
daxaur:add-ctrl-skill

Conversation

@daxaur

@daxaur daxaur commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Adds skills/ctrl/SKILL.md — an on-chain automation skill that turns natural-language intents into CTRL workflows on Base.

What CTRL does

  • Visual workflow automation (trigger → action graphs) deployed to an audited V3 vault on Base mainnet.
  • Wallet signs once (EIP-5792 batch: deploy vault + register rules); a Render-hosted keeper polls every ~5s and executes from there.
  • Every execution is bounded by the maxPerSwap and maxPerDay caps the user signed.

What the skill does

Given a natural-language intent like "DCA 0.01 ETH into USDC every Monday at 14:00 UTC", the skill:

  1. Pulls the live block catalog (GET /api/mcp/block-catalog)
  2. Composes a trigger + action graph using the catalog ids
  3. Creates a draft workflow (POST /api/mcp/workflows)
  4. Requests the EIP-5792 activation batch (POST /api/mcp/activate/<id>)
  5. Sends the user a one-shot signUrl via ./notify — the agent never holds keys

Trust boundary

The REST surface at /api/mcp is intentionally anonymous — the security boundary is the wallet signature at activate-time, not API auth at create-time. Drafts that never get signed auto-prune.

Resources

Comment thread skills/ctrl/SKILL.md Outdated
"config": {
"tokenIn": "ETH",
"tokenOut": "USDC",
"amount": 0.01,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[ISSUE] Example DCA uses amount: 0.01 ETH but the default maxPerSwap: 0.005 ETH cap (line 65 of this file) means the CTRL keeper will silently reject every single swap — the vault signs successfully but never executes, giving the user no error feedback.

Comment thread skills/ctrl/SKILL.md Outdated
"tokenIn": "ETH",
"tokenOut": "USDC",
"amount": 0.01,
"slippage": 15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[ISSUE] slippage: 15 has no documented unit — if interpreted as percent (15%), every swap is wide open to sandwich attacks; if BPS (0.15%), it's fine. The block catalog likely defines this field, but the skill never says so, leaving every future cypher.swap workflow with a 100× ambiguity in slippage tolerance.

@NeronCrypto NeronCrypto left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: discussion-needed — 2 issues in the example workflow block the feature from working correctly.

Findings (mirrored as inline comments):

  • [ISSUE] skills/ctrl/SKILL.md:59 — amount: 0.01 ETH exceeds the default maxPerSwap: 0.005 ETH cap; the CTRL keeper will silently reject every swap, making the example DCA non-functional despite a successful wallet signature.
  • [ISSUE] skills/ctrl/SKILL.md:60 — slippage: 15 has no documented unit (% vs BPS = 100× difference in sandwich exposure); implementors must infer the unit from the external block catalog, which is never referenced here.

@aaronjmars

Copy link
Copy Markdown
Owner

Thanks for this — CTRL is a genuinely cool fit for aeon, the API is live, and the safety model (agent never signs, only hands back an EIP-5792 signUrl) is well thought through. A few things need fixing before it can land, mostly convention + the worked example drifting from the live API:

1. Invalid capability value (blocker). capabilities: [external_api, on_chain, sends_notifications]on_chain isn't in the locked taxonomy (docs/CAPABILITIES.md); the parity check will drop it as an unknown value. It should be:

capabilities: [external_api, writes_external_host, onchain_writes, sends_notifications]

(onchain_writes is the valid term, and writes_external_host is needed since the skill POSTs to ctrl.build.)

2. Missing registration (blocker). Adding a skill here touches four files — see the merged vigil-revoke PR (#354) as the template: skills/ctrl/SKILL.md + an aeon.yml entry (register it, enabled: false, schedule: workflow_dispatch) + a regenerated skills.json. This PR only adds the SKILL.md, so CTRL is currently undiscoverable and skills.json is stale.

3. Worked example doesn't match the live catalog. I diffed the SKILL.md against GET /api/mcp/block-catalog:

  • The catalog jq {actions: [.chain[].id]} references a .chain[] key that doesn't exist — actions live under .actions[], so it returns null.
  • The DCA example uses time.cron, but the live catalog only has time.interval — the example would fail at the keeper.
  • Example amount: 0.01 ETH exceeds the skill's own maxPerSwap: 0.005 cap, so the keeper rejects every swap even after a good signature.
  • slippage: 15 has no unit — % vs BPS is a 100× difference in sandwich exposure; please pin it.

4. Nice-to-have (security): before forwarding the returned signUrl to the user via ./notify, validate it points at ctrl.build — the catalog endpoint is anonymous, so a spoofed response could otherwise surface an attacker signUrl.

Fix 1–3 and I'll merge. Appreciate the contribution!

Comment thread skills/ctrl/SKILL.md

```bash
WORKFLOW=$(curl -m 15 -s -X POST "https://ctrl.build/api/mcp/workflows" \
-H "Content-Type: application/json" -d @body.json)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[ISSUE] curl -d @body.json references a file that is never written — the skill composes the workflow JSON in step 2 as a conceptual block, but step 3 jumps straight to curl ... -d @body.json with no printf '%s' "$WORKFLOW_JSON" > body.json preceding it. An agent executing this literally will get curl: (26) Failed to open/read local data from file/application and fall into the error branch, never creating the draft. Add an explicit write step before the curl.

Comment thread skills/ctrl/SKILL.md Outdated
```bash
BATCH=$(curl -m 15 -s -X POST "https://ctrl.build/api/mcp/activate/${WID}" \
-H "Content-Type: application/json")
SIGN_URL=$(printf '%s' "$BATCH" | jq -r '.signUrl')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[ISSUE] No null/empty guard on SIGN_URL — if the activate endpoint returns unexpected JSON or the field is absent, jq -r '.signUrl' outputs the string "null", and step 5 sends that unclickable string to the user with no error signal. Add [ -n "$SIGN_URL" ] && [ "$SIGN_URL" != "null" ] || { echo "CTRL_ACTIVATE_FAILED"; exit 1; } immediately after this line, mirroring the WID check in step 3.

@NeronCrypto NeronCrypto left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: discussion-needed
Two steps reference state that is never set up, leading to silent failure paths.

Findings (mirrored as inline comments):

  • [ISSUE] skills/ctrl/SKILL.md:75 — curl -d @body.json references a file that step 2 never writes; executing this literally yields curl: (26) and the workflow is never created. Add an explicit printf '%s' "$WORKFLOW_JSON" > body.json before the curl.
  • [ISSUE] skills/ctrl/SKILL.md:87 — SIGN_URL has no null/empty guard after jq -r '.signUrl'; if activate returns unexpected JSON, user receives the literal string "null" as the sign URL with no error. Add the same [ -n "$SIGN_URL" ] && [ "$SIGN_URL" != "null" ] || ... pattern used for WID in step 3.

@daxaur

daxaur commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

pushed fixes for all three blockers + neron's notes:

  1. capabilities now [external_api, writes_external_host, onchain_writes, sends_notifications] per docs/CAPABILITIES.md — dropped the invalid on_chain.
  2. registered it properly: aeon.yml entry (enabled: false, workflow_dispatch), category in generate-skills-json, and the skills.json entry. used feat: vigil-revoke skill — closes detection→revoke loop via Bankr #354 as the template.
  3. re-diffed the worked example against the live block-catalog — time.crontime.interval (only schedule trigger that exists), fixed the jq to .actions[], buy amount down to 0.005 under the 0.01 cap, and pinned slippage to percent. also write body.json before the create curl now.

on the signUrl spoofing note — the skill never parses a returned url, it builds https://ctrl.build/activate/<id> itself, so there's nothing to spoof. one thing worth flagging: the agent never signs at all, the wallet does — so onchain_writes is technically broader than what the skill does, but happy to keep it as the conservative blast-radius hint.

thanks for the thorough review.

@aaronjmars

Copy link
Copy Markdown
Owner

Hey @daxaur — still keen to land this. I did another deep pass today; the design is right (agent never signs, one-shot signUrl, conservative caps) and the API checks out live. What's left:

From my last review (still pending):

New asks:

  • The vault contract 0x5Df2…1DBB is unverified on Basescan — please verify the source and link the audit backing the "audited V3 vault" claim
  • Make the signUrl host check (signUrl host == ctrl.build) a hard step before notify, not a nice-to-have
  • Drop the "retry via WebFetch if curl is blocked" sandbox note
  • Small ones: catalog says 24 blocks (skill says 23), and ${USER_WALLET} in the follow-ups is never defined

Separately — I added the CTRL MCP and tried it. @daxaur/ctrl-mcp 0.2.0-hosted at www.ctrl.build/api/mcp responds well: initialize + tools/list work anonymously, the 6 tools look sensible, and the instructions block is good. Two notes: (1) tool execution requires an sk_ctrl_ key while the skill's REST flow is fully anonymous — worth aligning or documenting which surface is canonical; (2) auth failures return bare {"error": "..."} JSON instead of JSON-RPC error objects (no jsonrpc/id/code), which strict MCP clients will choke on.

Push those and I'll merge promptly.

@aaronjmars

Copy link
Copy Markdown
Owner

Hey @daxaur — following up on my note above. Heads up that this now also has a merge conflict with main (my recent merges shifted the base under it), so it can't land as-is. When you get a chance, could you rebase onto the latest main? The conflict is almost certainly just the skills registry — fine to resolve as a union (keep both sides). Still keen to land it once it's green. 🙏

@daxaur

daxaur commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

rebased onto latest main (the registry files drift fast). everything from your last pass is in now:

  • dropped the WebFetch note, defined ${USER_WALLET}, added the new withdrawal tool
  • contracts are verified on basescan — V13 factory 0x5Df25e79efd7f9dc86841b404b3EA6F4b7951DBB, vault impl 0xc98137a6df7fb91ab91b568bf923490a278aa702, timelock beacon 0x5760A6D62743860F27843fA314E22166dBEF7d73; the "audited" wording is gone across our surfaces (Slither + manual review, formal audit planned before V1)
  • capabilities, registration, and the worked example (time.interval, .actions[], amount under cap, slippage in %) were all already in from the prior push

also fixed the two things you flagged on the MCP server itself: auth failures now return proper JSON-RPC error objects ({jsonrpc, id, error: {code, message}}), and the instructions block now says which surface is canonical — anonymous REST for wallet-native agents, key-gated JSON-RPC for desktop clients.

should be clean to merge now. thanks for the thorough reviews! 🩶

daxaur added 3 commits June 12, 2026 10:04
Adds an on-chain automation skill that compiles natural-language intents
(DCA, price-gated swaps, launchpad sniping) into a CTRL workflow on Base.
The wallet signs an EIP-5792 batch once; the CTRL keeper handles every
trigger after, bounded by the per-swap and per-day caps the user signs.

The REST surface at /api/mcp is anonymous — the security boundary is
the wallet signature at activate-time, not API auth at create-time.
Agents never hold keys; activation returns a hosted signUrl the user
opens in their wallet.
…ed example

- capabilities: drop invalid on_chain → [external_api, writes_external_host,
  onchain_writes, sends_notifications] (matches docs/CAPABILITIES.md)
- register the skill the way the rest of the repo does: aeon.yml entry
  (enabled: false, workflow_dispatch), generate-skills-json category, and the
  regenerated skills.json entry. was undiscoverable before.
- worked example now matches the live /api/mcp/block-catalog: time.interval
  instead of the nonexistent time.cron, .actions[] in the jq, buy amount 0.005
  under the 0.01 cap, slippage pinned to percent.
- write body.json explicitly before the create curl, and the activate URL is
  built as https://ctrl.build/activate/<id> so there's no returned signUrl to
  spoof.
@aaronjmars aaronjmars merged commit f32b27d into aaronjmars:main Jun 12, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants