Agents Onchain 2026 · Autonomous CFO for onchain merchant escrow

Coach decides.
KeeperHub ships.

Kajota × KeeperHub is an escrow release rail for onchain merchants. Coach — a Gemini-backed agent — watches deposits, applies deterministic rules to decide releases, narrates the reasoning in plain English, and audits its own KH workflow against the trap catalogue we shipped in KH's canonical docs. KeeperHub's Turnkey wallet signs the release under EIP-7702 delegation. All in ~15 seconds from the moment Coach greenlights the release.

~15s
Release latency
85 / 15
USDC split
0
Keys Coach holds
✓
Bounty PR merged to KH
See a live release ↓ Coach reasoning ↓ The bounty docs ↓ ↗ Read merged PR #1857
01

Watch Coach refuse, then release.

Deposit 0.10 test USDC and Coach holds it — money moved, nothing else happened yet. Confirm receipt and it releases. A release layer that always says yes isn't a decision layer.

End-to-end: deposit → Coach holds → confirm → KeeperHub releases.

Connect a Sepolia wallet and put down 0.10 test USDC. You'll sign one approve and one deposit — and then Coach will refuse to release, printing the rule that failed. Confirm receipt and the same rules pass; only then does KeeperHub sign. You never pay release gas.

// events land here

No wallet? Replay the demo.

This depositId is already released — a second attempt reverts on-chain with the escrow's idempotency guard. Real KH keeper signing path, no USDC required.

depositId… demo
Recent runs
Status
Duration
Tx / Error
Started
Loading…
… Coach on its own — the same rules, on a timer, with no browser open idle

A server-side loop polls Sepolia for every deposit handed to it, reads escrow state with a raw eth_call, and runs the identical rules engine. When the verdict is release it invokes the KeeperHub workflow directly — no click, no wallet, no page loaded. It ships dry-run by default: chain reads and rule evaluation are real, only the signature is withheld until an operator sets KH_WATCHER_LIVE=1.

—
Watching
—
Released by Coach
—
Ticks run
—
Next tick
Watcher log
// loading…
02

Coach — the CFO and the auditor.

Two live HTTP endpoints, callable by any agent. The CFO decides whether a release should ship; the auditor catches KeeperHub workflow traps before they fire.

Audit any KeeperHub workflow.

Same rules the deployed POST /coach/audit-workflow endpoint runs — ported to the browser so it's instant, no server round-trip. Load a canned demo (bad / clean) or paste your own workflow JSON to see the report.

Seed

Coach as CFO — three verdicts.

Each button POSTs /concierge/coach/should-release with one signal changed and renders whatever comes back. Deterministic rules produce release hold reject, and a narration explains why. Same signals in → same verdict out, every time.

POST https://kajota-hub.onrender.com/concierge/coach/audit-workflow live
// Content-Type: application/json
{
  "workflow": {                         // the KH workflow definition, verbatim
    "name": "Release escrow on Sepolia",
    "nodes": [
      {
        "id": "trigger-1",
        "type": "trigger",
        "data": { "label": "HTTP",
                  "config": { "triggerType": "HTTP" } }
      },
      {
        "id": "step-1",
        "type": "action",
        "data": { "config": {
          "actionType":     "web3/write-contract",
          "network":        "11155111",
          "web3Connection": "default",
          "contractAddress":"0x5998…1776",
          "abiFunction":    "release",
          "functionArgs":   "[\"{{@trigger-1:HTTP.depositId}}\"]",
          "abi":            "[{…}]"
        } }
      }
    ],
    "edges": [ { "source": "trigger-1", "target": "step-1" } ]
  },
  "workflowRef": "optional-label"       // echoed back in the report
}
// 200 OK — a workflow that trips every trap
{
  "passed": false,
  "counts": { "error": 5, "warn": 1, "info": 1 },
  "issues": [
    {
      "trap":     "silently-ignored-integration-id",
      "severity": "error",          // error | warn | info
      "path":     "nodes[1].data.config.integrationId",
      "detail":   "`integrationId` is a reserved config key that the
                   validator accepts but the write-contract action
                   ignores. The signing wallet is routed by
                   `web3Connection` instead.",
      "fix":      "replace with `\"web3Connection\": \"default\"`"
    }
    // … one object per detected trap
  ],
  "summary": "Audit failed: 5 errors (+ 1 warning) across 1
              write-contract action. Fix errors before executing…",
  "actionNodesScanned": 1,
  "workflowRef": "optional-label"
}
# paste this straight into a terminal — returns a full report card
curl -sS -X POST https://kajota-hub.onrender.com/concierge/coach/audit-workflow \
  -H 'content-type: application/json' \
  -d '{"workflow":{"name":"probe","nodes":[{"id":"s","type":"action",
       "data":{"config":{"actionType":"web3/write-contract",
       "function":"release","integrationId":"int_x",
       "functionArgs":["{{@trigger.body.depositId}}"]}}}],
       "edges":[]}}'

# GET the same URL in a browser for a self-describing info payload
curl -sS https://kajota-hub.onrender.com/concierge/coach/audit-workflow
FieldTypeNotes
workflow object required A KeeperHub workflow definition — name, nodes[], edges[]. Paste it verbatim from GET /api/workflows/{id} or from the editor's JSON view.
workflowRef string Optional label echoed back on the report. Handy when auditing many workflows in a batch.
passed bool ↩ response true when zero error-severity issues. Warnings and info never block.
issues[].severity enum error — mis-routes or fails at execute time · warn — accepted but non-canonical · info — advisory.
issues[].fix string A copy-pasteable correction, not just a description of the problem.
actionNodesScanned int How many web3/write-contract nodes were examined. 0 means nothing in this workflow is in scope.
03

What the auditor is checking for.

Three gaps we hit, fixed upstream, and now enforce in code. One field is silently ignored, one only errors at exec time, one template syntax works only in stored workflows.

web3Connection, not integrationId.

The most dangerous kind of doc gap: the wrong field is accepted. Set integrationId to route the signing wallet and the API validates it, saves it, and then ignores it entirely — your write goes out on whatever wallet the org policy resolves. The real routing key is web3Connection (accepts "default", "eoa", or "safe:<id>").
You'll try { // accepted → silently ignored "integrationId": "int_your-keeper" }
Actually routes the wallet { "web3Connection": "default" // or "eoa", or "safe:<safeWalletId>" }

functionArgs is a JSON string.

The docs example shows an array. The runtime wants a stringified JSON array. Pass the raw array and it accepts, stores, then throws Invalid function arguments JSON at execution — half an hour after your last save.
You'll try { "functionArgs": [ "{{@trigger.body.depositId}}" ] }
Actually works { "functionArgs": "[\"{{@trigger-1:HTTP.depositId}}\"]" }

Trigger templates need stored syntax.

The intuitive template — {{@trigger.body.x}} — silently resolves to nothing. Stored HTTP triggers use a slot-scoped path: {{@trigger-1:HTTP.x}}. And the POST body must be wrapped under input.
You'll try // template {{@trigger.body.depositId}} // POST body { "depositId": "0x…" }
Actually works // template {{@trigger-1:HTTP.depositId}} // POST body { "input": { "depositId": "0x…" } }
Full UI-label → API-field mapping · as merged into KeeperHub’s docs
UI label (dashboard)
API field (workflow body)
Gotcha
Function
abiFunction
Canonical. API rejects function and method; functionName works as an undocumented legacy alias.
Function Arguments
functionArgs
Must be a JSON-encoded string, not a raw array. Errors surface at exec, not create.
Contract ABI
abi
Also a JSON-encoded string. Only the specific fragment KH will call is required.
Web3 Connection
web3Connection
Sender routing: "default" (org policy), "eoa", or "safe:<id>". The wallet is your org's Turnkey wallet, resolved automatically. integrationId looks like the right field but is silently ignored.
Contract
contractAddress
Only one that maps 1:1 with what you'd guess.
Trigger payload
{{@trigger-1:HTTP.x}}
Not {{@trigger.body.x}}. And your POST body must wrap fields under input.
04

Generate the workflow JSON.

Paste your function signature. Get the exact body KeeperHub accepts — all three traps pre-avoided.

Signature accepts one Solidity function like release(bytes32), transfer(address,uint256), or castVote(uint256,uint8). Everything on the right is generated live — copy the whole block into your KH workflow's web3/write-contract action.

Notice: web3Connection (trap #01, the silently-ignored field) · functionArgs as a JSON-encoded string (trap #02) · trigger template {{@trigger-1:HTTP.…}} (trap #03) · canonical abiFunction (with functionName also accepted as a legacy alias) · minimal abi with only the fragment KH parses at deploy time.

05

Live workflow (behind the proof).

The exact IDs and addresses the workflow calls. Verified live before PR #1857 was opened.
Workflow ID
…
Contract
Function
release(bytes32 depositId) · …
Keeper (EIP-7702)
Chain
…
KH dashboard