Skip to content

fix(keeperhub): swap on-chain setText for webhook pulses (α — keep KeeperHub firing, zero gas)#13

Merged
fritzschoff merged 1 commit intomainfrom
fix/disable-onchain-heartbeat-rep-cache
Apr 29, 2026
Merged

fix(keeperhub): swap on-chain setText for webhook pulses (α — keep KeeperHub firing, zero gas)#13
fritzschoff merged 1 commit intomainfrom
fix/disable-onchain-heartbeat-rep-cache

Conversation

@fritzschoff
Copy link
Copy Markdown
Owner

@fritzschoff fritzschoff commented Apr 29, 2026

Summary

The two KeeperHub workflows triggered after every paid x402 quote — Heartbeat (0zuje21a39euf7ow86f2s) and ReputationCache (in0hbqxlivyp34dchqufb) — each did a real Sepolia `setText` signed by `PRICEWATCH_PK`. ~14k gas burned per quote. A heartbeat that costs gas is a contradiction.

This PR keeps the workflows firing on every quote (the sponsor demo surface stays intact) but swaps their on-chain Web3 Write nodes for Webhook POST nodes that hit our app. The app updates Redis. The dashboard reads last-seen-at and reputation-summary from Redis, with the on-chain text record as a cold-cache fallback.

What changed

Two new endpoints

  • POST /api/keeperhub/heartbeat-pulse — bearer-auth via `KEEPERHUB_WEBHOOK_SECRET`. Writes `agent:1:last-seen` and `ens:dynamic:1:last-seen-at` to Redis (24h TTL). Calls `pushKeeperhubRun` so the run shows on `/keeperhub`.
  • POST /api/keeperhub/reputation-pulse — same auth. Reads `ReputationRegistry.feedbackCount(agentId)` (cheap on-chain view, no tx), writes `reputation:summary:1` and `ens:dynamic:1:reputation-summary`.

Read path

  • lib/ens.ts:resolveAgentEns now prefers Redis-backed `ens:dynamic:*` keys for the two dynamic fields, falling back to the on-chain text record only if Redis is empty. Dashboard heartbeat pill stays live; on-chain text records freeze and become irrelevant.

KeeperHub side

The two workflows have already been updated via `update_workflow` MCP call (script at `/tmp/swap-workflows-to-webhooks.ts`):

Net result

Concern Before After
KeeperHub fires per quote
Workflow runs visible on /keeperhub
Dashboard heartbeat pill live
Per-quote Sepolia gas burn ~14k gas 0 gas

Forward path

W2 (issue #11, plan in PR #16) ships the CCIP-Read offchain resolver that exposes these Redis values as proper ENS text records readable from any wallet/etherscan/wagmi client. After W2 lands, the Redis pulse becomes the authoritative source and the on-chain text records can be deleted entirely (W3 plan #15).

Vercel env

Set KEEPERHUB_WEBHOOK_SECRET (Production already done; matches INFT_ORACLE_API_KEY for now — different in production hardening if needed).

Test plan

  • `pnpm typecheck` clean
  • `pnpm build` clean (both pulse routes registered)
  • Local smoke: happy path returns 200 + parsed timestamp; bad auth returns 401; reputation pulse reads on-chain count and writes Redis
  • After merge: trigger a paid x402 quote on prod, confirm the heartbeat workflow's KeeperHub run shows "completed" and the dashboard heartbeat pill updates within 30s
  • After merge: confirm `PRICEWATCH_PK` Sepolia balance does NOT decrease per-quote anymore

🤖 Generated with Claude Code

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hackagent Ready Ready Preview, Comment Apr 29, 2026 1:15pm

Request Review

Per-quote KeeperHub workflow runs were doing real Sepolia setText txs
signed by PRICEWATCH_PK — ~14k gas burned per quote just to write
last-seen-at and reputation-summary text records. Worse, this was the
KeeperHub demo surface for the hackathon: the workflows ARE the
sponsor story, deleting them is wrong.

Fix: keep the workflows firing on every paid x402 quote (sponsor story
intact) but replace their Web3 Write nodes with Webhook POST nodes
that hit our app instead.

  /api/keeperhub/heartbeat-pulse
    - validates KEEPERHUB_WEBHOOK_SECRET bearer
    - writes timestamp to Redis: agent:1:last-seen +
      ens:dynamic:1:last-seen-at (24h TTL)
    - calls pushKeeperhubRun so the run is visible on /keeperhub
    - returns 200 with the updated lastSeenAt ISO

  /api/keeperhub/reputation-pulse
    - same auth shape
    - reads ReputationRegistry.feedbackCount (cheap on-chain view, no tx)
    - writes summary to Redis: reputation:summary:1 +
      ens:dynamic:1:reputation-summary (24h TTL)
    - calls pushKeeperhubRun, returns 200

lib/ens.ts:resolveAgentEns now prefers the Redis-backed ens:dynamic:*
keys for last-seen-at + reputation-summary, falling back to the
on-chain text record only if Redis is empty. Dashboard heartbeat pill
stays live; on-chain text records freeze and become irrelevant.

Both KeeperHub workflows already updated via update_workflow MCP call
(see /tmp/swap-workflows-to-webhooks.ts).

W2's CCIP-Read gateway will expose these Redis values as proper ENS
text records readable from any client. This is W2-α reduced to a
single record pair — the W2 plan still ships the full version.

Net result:
  - KeeperHub fires per quote ✓ (sponsor story)
  - Workflow runs visible on /keeperhub ✓
  - Dashboard heartbeat live ✓
  - Zero gas burn ✓

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@fritzschoff fritzschoff force-pushed the fix/disable-onchain-heartbeat-rep-cache branch from b0fdd02 to 514c9eb Compare April 29, 2026 13:14
@fritzschoff fritzschoff changed the title fix: stop on-chain heartbeat + reputation-cache triggers (gas drain) fix(keeperhub): swap on-chain setText for webhook pulses (α — keep KeeperHub firing, zero gas) Apr 29, 2026
@fritzschoff fritzschoff merged commit f64e490 into main Apr 29, 2026
2 checks passed
@fritzschoff fritzschoff deleted the fix/disable-onchain-heartbeat-rep-cache branch April 29, 2026 13:16
fritzschoff added a commit that referenced this pull request Apr 30, 2026
…'re now webhook-only post PR #13)

Adds scripts/setup-keeperhub-workflows.ts that calls create_workflow via KeeperHub MCP
for ENSPrimaryNameSetter, ENSAvatarSync, and GatewayCacheInvalidator.
Supports --dry-run flag. Skips deletion of heartbeat/reputation-cache (both
converted to webhook-only triggers in PR #13).

Workflow IDs created:
  KEEPERHUB_WORKFLOW_ID_PRIMARY_NAME=x3x1yxn1i9fi6qs63v4lu
  KEEPERHUB_WORKFLOW_ID_AVATAR_SYNC=iosfz5m65htyd18be78sp
  KEEPERHUB_WORKFLOW_ID_GATEWAY_INVALIDATE=3tzmhfpvsnom1bnkeieoz

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fritzschoff added a commit that referenced this pull request Apr 30, 2026
…15)

* docs: W3 implementation plan (primary names + KeeperHub orchestration)

13 tasks across 7 milestones:
  M1 reverse names for locally-keyed wallets (AGENT_PK, PRICEWATCH_PK,
     VALIDATOR_PK) on Sepolia + Base Sepolia
  M2 KeeperHub workflow provisioning — 4 new (PrimaryNameSetter,
     AvatarSync, GatewayCacheInvalidator, OnboardAgent), 2 deleted
     (Heartbeat, ReputationCache)
  M3 webhook integrations + event-firehose cron
  M4 primary name for Turnkey wallet via PrimaryNameSetter workflow
  M5 OnboardAgent end-to-end (6 orchestrated steps)
  M6 e2e tests
  M7 PR + walkthrough

Depends on W2 — gateway needs to handle nested wallet labels
(agent-eoa.tradewise.agentlab.eth etc). The plan extends labelToAgent
in lib/ens-gateway.ts in M1 Task 1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(w3): gateway handles nested wallet labels (agent-eoa, pricewatch-deployer, validator)

Add WALLET_LABELS map and addressOverride field to AgentInfo so that
direct wallet subnames (agent-eoa.tradewise.agentlab.eth,
pricewatch-deployer.agentlab.eth, validator.agentlab.eth,
keeperhub.agentlab.eth) resolve to their respective addresses without
hitting the agentId-based Edge Config lookup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(w3): correct addr CCIP-Read encoding from bytes to address type

addr(bytes32) return type is `address` not `bytes`. viem getEnsAddress
decodes resolveWithProof result as abi.decode(result,(address)), so
the gateway must encode as abi.encode(address) — not abi.encode(bytes).
This fixes getEnsName forward-validation which was returning 0x20 (the
bytes offset pointer) instead of the actual wallet address.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(w3): scripts/setup-primary-names — set reverse name per chain idempotently

One-shot runner for AGENT_PK, PRICEWATCH_PK, VALIDATOR_PK. Checks
balances upfront, verifies registrar bytecode (graceful degradation for
missing L2 deployer), skips if reverse name already matches. Runs on
Sepolia (ReverseRegistrar 0xA0a1...) and Base Sepolia (ENSIP-19 L2
reverse registrar 0x00000BeEF...) with per-chain ABI variants.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(w3): keeperhub-workflows — typed builders for 3 new workflow shapes (PrimaryName, AvatarSync, GatewayInvalidate)

Adds lib/keeperhub-workflows.ts with WorkflowSpec type and three builders:
- buildEnsPrimaryNameSetter: dual-chain Web3 Write setName on Sepolia + Base Sepolia
- buildEnsAvatarSync: Web3 Write setText avatar on Sepolia PublicResolver
- buildGatewayCacheInvalidator: webhook-to-webhook relay (zero gas)
Extends KeeperHubKind in edge-config.ts with primary-name, avatar-sync, gateway-invalidate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(w3): provision 3 new workflows (heartbeat + rep-cache stay; they're now webhook-only post PR #13)

Adds scripts/setup-keeperhub-workflows.ts that calls create_workflow via KeeperHub MCP
for ENSPrimaryNameSetter, ENSAvatarSync, and GatewayCacheInvalidator.
Supports --dry-run flag. Skips deletion of heartbeat/reputation-cache (both
converted to webhook-only triggers in PR #13).

Workflow IDs created:
  KEEPERHUB_WORKFLOW_ID_PRIMARY_NAME=x3x1yxn1i9fi6qs63v4lu
  KEEPERHUB_WORKFLOW_ID_AVATAR_SYNC=iosfz5m65htyd18be78sp
  KEEPERHUB_WORKFLOW_ID_GATEWAY_INVALIDATE=3tzmhfpvsnom1bnkeieoz

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(w3): wire ENSAvatarSync + GatewayCacheInvalidator triggers from confirm-transfer

Adds triggerKeeperHubByKind wrapper to lib/keeperhub.ts (validates kind string
against KeeperHubKind, delegates to existing triggerKeeperHub). Fires both
avatar-sync and gateway-invalidate as fire-and-forget .catch() triggers at the
end of the confirm-transfer oracle route after Redis key rotation succeeds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(w3): keeperhub.agentlab.eth maps to actual Turnkey address (was placeholder)

W3 M4 confirmed the Turnkey-managed wallet's address from the on-chain
setName tx logs: 0xB28cC07F397Af54c89b2Ff06b6c595F282856539. Forward
addr lookup must match the reverse record for getEnsName to return the
label — viem does the round-trip check per ENSIP-19.

The earlier placeholder was 0x0000…0000 and was based on a typo in M2's
report (extra 'c'). Tx 0xf07068…0d56e on Sepolia is the canonical setName
that established the reverse record.

* test(w3): e2e for primary names + manual walkthrough doc

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(w3): force-dynamic /inft + 8s timeout on ENS reads (build was timing out)

PR #15's preview build kept failing because the /inft page does 5
parallel CCIP-Read calls during static generation, each calling the
production gateway URL. Total exceeded the 60s build-step budget and
Vercel marked the deploy Error.

Two fixes:
1. /inft page: export const dynamic = "force-dynamic" — render at
   request time, not build time. Each viewer gets fresh data anyway
   since the W2 gateway returns live values.
2. lib/ens-records.ts: 8s timeout per getEnsText call. If the gateway
   is slow or unreachable, the page renders with null fallbacks
   instead of hanging.

Both are defense in depth — even if one of the 5 reads times out,
the page still renders and the user sees a "—" instead of an error.

* fix(w3): force-dynamic on / too (resolveAgentEns goes through CCIP-Read)

* docs: extend /docs deep-dive with W2 + W3 sections

Adds 5 new sections (∇09–∇13) below the existing W1 architecture deep
dive so judges see the full agent-identity package end-to-end:

  ∇09 W2 — CCIP-Read ENS gateway (architecture + trust posture)
  ∇10 Resolve flow — what happens when wagmi/viem queries an ENS record
  ∇11 Live records served by the gateway (full table)
  ∇12 W3 — ENSIP-19 multichain primary names (4-wallet table)
  ∇13 W3 — KeeperHub orchestration (6-workflow table + cross-link explanation)

Also extends the contract-addresses ledger (∇14, was ∇08) with W2's
OffchainResolver + INFT_GATEWAY signer + ENS Registry, plus the W3
wallet→name mappings.

Three new sub-nav rows up top so the doc TOC reflects the structure.
Helper components RecordRow / NameRow / WfRow added at the bottom.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant