Skip to content

release: to prod#913

Merged
eskp merged 59 commits intoprodfrom
staging
Apr 21, 2026
Merged

release: to prod#913
eskp merged 59 commits intoprodfrom
staging

Conversation

@eskp
Copy link
Copy Markdown

@eskp eskp commented Apr 21, 2026

Summary

Promote the following merged PRs from staging to prod:

Risk callouts

Post-deploy verification

  • deploy-keeperhub workflow finishes green
  • curl -fsS https://app.keeperhub.com/api/health returns 200
  • Smoke-test the surfaces affected by the merged PRs above
  • Watch Sentry / logs for ~10 minutes after the rollout

joelorzet and others added 30 commits April 15, 2026 14:17
…ents

Consolidate native and ERC20 balance fetchers onto a shared rpcCall helper
with exponential backoff for HTTP 429, 5xx, network errors, and malformed
gateway responses with missing result fields. The missing-result case was
surfacing as BigInt(undefined) on the analytics page.
…ress validation

Split rpcCall and helpers out of fetch-balances.ts into lib/wallet/rpc.ts so
the retry logic is unit-testable in isolation. Add randomized jitter (up to
30%) to the backoff to avoid lockstep retries, and clamp the absolute maximum
delay to 5s regardless of schedule. Validate EVM addresses before encoding
balanceOf call data so an oversized input cannot silently produce wrong call
data via padStart. Emit a Sentry breadcrumb on every retry attempt so the
retry history is attached to any captured exception.

Add lib/wallet/rpc.test.ts covering encodeBalanceOfCallData validation,
hexWeiToBigInt edge cases, getRpcBackoffMs jitter and cap behavior, and all
rpcCall retry/throw branches.
…fails

Wire the chain-level default_fallback_rpc through ChainData so the wallet
balance fetchers can fail over when the primary RPC is throttled or down.
Add rpcCallWithFailover that walks the URL list in order with a reduced
per-URL retry budget so the handoff is fast. Emit rpc.failover breadcrumbs
for every hop. The user now sees an error only when every configured RPC
is exhausted, not when the primary alone gets a 429.
Two nodes could be connected multiple times via the same source/target
handle combination. Allow multiple connections between a node pair only
when they use different handles (e.g. Condition true/false to the same
target); reject exact duplicates.

- Extract hasDuplicateEdge to lib/workflow/edge-helpers.ts
- Reject duplicates in isValidConnection (drag-time) and onConnect
  (after sourceHandle auto-assignment)
The pane right-click menu exposed Add Step on the landing page because
onPaneContextMenu was wired unconditionally while the canvas is shared
between / and /workflows/[id] via PersistentCanvas. Other add-node
entry points (toolbar, AddStepButton, onConnectEnd) already gate on
isWorkflowRoute or require a real node to be present.

Extend the existing isGenerating guard with !isWorkflowRoute so the
handler resolves to undefined on the landing page; the canvas already
derives isWorkflowRoute from the pathname.
The AI prompt path bypassed isValidConnection/onConnect by calling
setEdges directly with workflow data. A hallucinated duplicate would
slip through into the canvas and the DB.

- Add dedupeEdges helper (O(n) via Set keyed on source/handle/target).
- Apply it to the streaming setEdges and to finalEdges before
  workflow.create/update.
- Cover with 5 additional unit tests.
- Move Billing from left nav to user menu dropdown (gated on isOwner + isBillingEnabled; routes to /billing)
- Move Address Book from user menu to left nav (replaces Billing slot); opens overlay on click
- Add Report an issue button to left nav bottom section (below Documentation); opens FeedbackOverlay
- New user menu order: Wallet, Settings, Connections, API Keys, Billing, Projects and Tags
- Combine separate Projects and Tags overlays into one tabbed ProjectsAndTagsOverlay
  (mirrors SettingsOverlay pattern; supports initialTab prop)
- NavItem ACTION_ITEM_IDS allowlist so href=null items that open overlays are not marked
  "Coming Soon"
…olish

UI redesign (/billing)
- PricingTable mirrors landing /pricing: pill Monthly/Annual toggle with inline
  "Save 20%" badge; centered 4-col grid at xl; HeroMetrics stat panel per card;
  custom TierSelect dropdown with green highlight for selected tier
- Outline current plan with a thin keeperhub-green-dark border + "CURRENT" badge
  instead of the old POPULAR highlight
- Enterprise card consistent with others ("Custom" price, "Talk to us" CTA,
  mailto:human@keeperhub.com); no gradient background or special border
- Shared ComparisonTable below the grid: "Compare all features" toggle reveals
  a 10-row striped matrix with Enterprise column accented in green
- CTA label normalized to "Change plan" for all paid plan changes
- Remove subheadings under price; remove redundant executions pill on Current
  Plan card; move renewal message (e.g. "Your plan ends on ...") inline with
  plan name + status pill
- Execution usage bar and gas credits bar restyled on keeperhub-green tokens
  with subtle /15 track; overage tail in yellow
- FAQ link footer at bottom of /billing routes to https://keeperhub.com/pricing

Billing Details card
- New BillingDetails card rendered next to BillingHistory (lg:grid-cols-[2fr_1fr])
- Fetches GET /api/billing/billing-details; shows card brand + last4 + expiry
  and invoice email; empty state when no card on file
- Edit pencil inline next to "Billing Details" title; opens Stripe portal

Auth gate
- billing-page.tsx mirrors analytics page pattern: useSession + local AuthGate,
  early-return when anonymous (no more pricing table exposure to logged-out users)

Data plumbing
- Add BillingDetails type and getBillingDetails(customerId) to BillingProvider
- Stripe implementation cascades: customer.invoice_settings.default_payment_method
  -> subscription.default_payment_method -> first customer payment method
  (Stripe Checkout attaches card to subscription, not customer, by default)
- Add BILLING_DETAILS to BILLING_API constants
- New GET /api/billing/billing-details route (owner-auth)
- /billing bumps refreshKey 2s after ?checkout=success so BillingDetails
  remounts once Stripe has attached the payment method
- BillingHistory view/PDF links recolored keeperhub green
- Confirm plan change dialog copy: remove leading "--" before the prorated
  billing note

Tests
- billing-handle-event.test.ts mock provider stubs getBillingDetails
…onnections

fix: KEEP-287 prevent duplicate edges between same nodes
The gate is subtle (the canvas is shared between / and /workflows/[id]
via PersistentCanvas). A comment at the call site prevents a future
maintainer from stripping !isWorkflowRoute as "cleanup to match siblings".
- New AppBanner client component mounted in app/layout.tsx: fixed 36px strip
  at the top of the app, keeperhub-green tint with border, centered info icon
  + body + "See plans" link, close (X) at right edge
- Dismissal is permanent-per-browser via localStorage key kh-billing-announce-v1
  so the banner never reappears for a user who closes it (version suffix lets
  us introduce a new banner later without wiping other prefs)
- Banner height is exposed via --app-banner-height CSS var on <html> so fixed
  overlays shift down cleanly when visible and snap back on dismiss. Updated:
  - components/workflow/workflow-toolbar.tsx (persistent toolbar top)
  - components/navigation-sidebar.tsx (sidebar top-[60px] now includes banner)
  - components/flyout-panel.tsx (two fixed surfaces)
  - app/workflows/[workflowId]/page.tsx (side panel lg breakpoint)
  - components/billing/billing-page.tsx (pt-20 -> calc)
  - components/analytics/analytics-page.tsx (pt-20 -> calc)
  - components/earnings/earnings-page.tsx (pt-20 -> calc)
- No hydration flash: component renders null until mounted to avoid SSR/client
  mismatch reading localStorage
The trigger node is the anchor of a workflow and cannot be deleted.
Remove the non-functional delete affordance from the properties tab
and suppress the node-level context menu on trigger nodes.
…ode-creation

fix: KEEP-289 gate pane context menu to workflow routes
…elete-option

fix: KEEP-290 hide delete option on trigger nodes
Add key={selectedNode.id} on the per-node config wrapper so the entire
subtree unmounts and remounts when the user selects a different node.
Without this, leaf components (AbiFunctionArgsField.localArgValues,
FieldGroup.isExpanded, etc.) retained useState from the previous node
because React preserves state for components at the same tree position
across prop changes. Result: field inputs from the old node persisted
in the panel after clicking a new node.
…sValid

The DOM-based hit-test (event.target.closest('.react-flow__node'|'.react-flow__handle'))
is unreliable on mouseup during an xyflow connection drag. event.target can be the
connection-line overlay rather than the target handle, so a successful handle-to-handle
drop fired both onConnect (correct edge) and onConnectEnd's fallback "create node on
pane drop" branch (spurious node + spurious edge).

Replace the DOM check with xyflow's own tri-state signal: FinalConnectionState.isValid.
Only null (pointer never entered a handle's connection radius) represents a true pane
drop. true means onConnect already handled it; false means an invalid handle drop.
…onnect

fix: KEEP-288 guard onConnectEnd node creation with connectionState.isValid
…nel-on-node-select

fix: KEEP-291 config panel retains previous node's field inputs
…ld-ecr

fix: KEEP-293 tolerate missing ECR credentials for Dependabot PR builds
Bumps the npm_and_yarn group with 1 update in the /docs-site directory: [dompurify](https://github.com/cure53/DOMPurify).


Updates `dompurify` from 3.3.3 to 3.4.0
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](cure53/DOMPurify@3.3.3...3.4.0)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.4.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
The CDP Bazaar (agentic.market) crawls paid x402 endpoints and looks for
extensions.bazaar.discoverable:true plus category/tags before indexing
the resource. Our prior work (KEEP-176) only emitted bazaar.schema for
agentcash/x402scan/mppscan; CDP Bazaar requires the discovery fields
separately.

Also fixes the resource.url in the dual-402 response, which threaded
request.url through and resolved to the internal pod bind
(https://0.0.0.0:3000/...) in K8s. buildPaymentConfig already used
NEXT_PUBLIC_APP_URL; buildDual402Response now does too.

- lib/payments/router.ts: discoverable:true always, category/tags when
  present, resource.url from NEXT_PUBLIC_APP_URL
- lib/x402/types.ts: project category, add tagName to CallRouteWorkflow
- app/api/mcp/workflows/[slug]/call/route.ts: leftJoin tags for tagName
- tests/unit/payment-router.test.ts: replace omit-extensions case,
  add category/tags case

Closes KEEP-294. Related: KEEP-176, KEEP-264.
…ite/npm_and_yarn-2a73d1bbcf

chore(deps): bump dompurify from 3.3.3 to 3.4.0 in /docs-site in the npm_and_yarn group across 1 directory
Canonical chain ID for Tempo mainnet is 4217 (already used throughout
the app, payment router, seed chains, and lib/rpc/types.ts). Three
stragglers still referenced the legacy 42420 at spec-comment-noted
blind spots from KEEP-176:

- keeperhub-events/event-tracker/lib/utils/chains.ts: dead constant
  (nothing in event-tracker imports AVAILABLE_CHAINS.TEMPO_MAINNET),
  update to 4217 for consistency.
- .claude/agents/protocol-domain.md: stale doc-prompt line referencing
  the Tempo Blockscout explorer by legacy ID. Actual explorer config
  in scripts/seed/seed-chains.ts:523 is keyed by 4217.
- lib/rpc/rpc-config.ts: remove the duplicate 42420 RPC entry. The
  canonical 4217 entry handles every in-app caller; nothing on our
  side resolves RPC by the legacy ID.

Adds a CI grep guard in pr-checks.yml (lint job) so future changes
can't reintroduce 42420 as a bare numeric literal in source. The
guard excludes *.md, drizzle/meta/*, and the workflow file itself.

Closes KEEP-261.
…revenue

Two new pages close the creator-facing and caller-facing documentation
gaps flagged in KEEP-259 after KEEP-176 shipped dual-protocol payments
without explaining to either audience what to expect.

- docs/workflows/paid-workflows.md -- creator-facing. Explains how x402
  settles on Base USDC and MPP settles on Tempo USDC.e, so a creator
  with balances on both chains understands the split is a function of
  which agents paid them (not a bug). Lists discovery scanners, pricing
  guidance, and points at the mcp-test dogfood reference.

- docs/ai-tools/agentcash-install.md -- caller-facing. Covers the
  one-liner `npx agentcash add https://app.keeperhub.com` that installs
  a KeeperHub skill into every supported AI agent's skill directory
  (17 at time of writing). Clarifies that agentcash handles per-call
  payment, no API key setup, and documents the two real meta-tools
  (search_workflows, call_workflow) that the skill exposes.

Both pages wired into their section _meta.ts and index.md.

This is the docs portion of KEEP-259. Wallet-overlay UI changes
(per-chain breakdown, first-visit explainer, Earnings card tooltip)
will ship in a follow-up PR on a separate branch.

Related: KEEP-259, KEEP-176, KEEP-294.
Add ABI-driven protocol definition for Aave V4 Hub-and-Spoke lending,
starting with the Lido Spoke on Ethereum mainnet.

- protocols/abis/aave-v4.json: reduced ABI covering 8 ISpoke functions
- protocols/aave-v4.ts: defineAbiProtocol with lidoSpoke contract
- tests/unit/protocol-aave-v4.test.ts: 19 unit tests

Actions mirror the V3 surface with V4 semantics:
supply, withdraw, borrow, repay, set-collateral,
get-user-supplied-assets, get-user-debt, and a new
get-reserve-id utility needed to resolve the opaque uint256
reserveId V4 uses to identify reserves within a Spoke.

Differences from V3 noted in the file: no referralCode on supply,
no interestRateMode on borrow/repay, onBehalfOf added to
setUsingAsCollateral. Write actions surface (shares, amount)
return values as named outputs.

Additional Spokes (EtherFi, Kelp, Ethena Correlated,
Ethena Ecosystem, Lombard BTC) share the same ABI and can be
added as further contract entries in follow-ups.

Notes:
- No Sepolia deployment exists yet; integration tests deferred.
- getUserAccountData intentionally excluded this pass - it
  returns a Solidity struct and the template engine path for
  nested tuple field access needs verification first.
Makes the dual-chain revenue split visible to creators on the Earnings
page so balances on Base and Tempo are both obviously real, not a bug.

- lib/earnings/types.ts: add perChain { base, tempo } with grossRevenue
  and invocationCount per chain.
- lib/earnings/queries.ts: new chain-grouped aggregation over
  workflow_payments using workflow_payments.chain. Extracted
  buildPerChainEarnings() for unit testing and to guarantee the UI
  always receives a fixed { base, tempo } shape (missing chain defaults
  to zero, unknown chains are ignored).
- components/earnings/earnings-kpi-cards.tsx: show the per-chain split
  as subtext on the Total Revenue and Total Invocations cards
  ("Base $X -- Tempo $Y"). Adds a HelpCircle info link on the Total
  Revenue and Earnings cards that opens the new paid-workflows docs
  page in a new tab.

Part 2 of KEEP-259. Part 1 (PR #909) added the underlying docs pages.

Not in scope (deliberately parked): first-visit dismissible explainer
overlay on the wallet overlay. The inline help link + docs covers the
disambiguation need; a separate dismissible modal adds surface area
with unclear measurement of whether it's seen. Revisit if creator
support volume shows the current affordance is insufficient.

Related: KEEP-259, KEEP-176, KEEP-294.
suisuss and others added 15 commits April 21, 2026 16:12
…ions.type

Schema audit surfaced two slug-bearing surfaces the original 0051 missed:

  * workflows.nodes[].data._eventProtocolSlug -- event trigger nodes store
    the protocol slug here (separate from action nodes' actionType /
    _protocolMeta). Modelled after the 0025 safe-wallet precedent using
    jsonb_set via jsonb_agg, since _eventProtocolSlug is a native jsonb
    key (unlike the stringified _protocolMeta).

  * integrations.type -- $type<IntegrationType>. Protocol plugins set
    requiresCredentials: false so no rows are expected in practice, but
    adding the rename is idempotent and cheap insurance.

Not touched:
  * _eventProtocolIconPath -- icon file itself is unchanged across the
    rename (protocols/aave-v3.ts still declares "/protocols/aave.png"),
    unlike the 0025 safe-wallet case which had to rewrite the icon path.
  * Historical execution tables (workflow_executions, workflow_execution_logs,
    direct_executions) -- rewriting them would falsify past-run history.

Migration header now lists the audited surfaces explicitly.
Previous pattern swallowed any error that didn't contain three specific
strings into a passing test:

  try {
    // positive assertions -- only run if RPC succeeds
  } catch (error) {
    expect(String(error)).not.toContain("INVALID_ARGUMENT");
    expect(String(error)).not.toContain("could not decode");
    expect(String(error)).not.toContain("invalid function");
  }

An RPC timeout, a rate-limit, an unrelated revert reason, or a genuine
ABI mismatch whose error string happened to miss those three needles would
all pass green with no positive assertion actually running.

Rewrite:
  - Read tests: remove try/catch. If provider.call fails or the return
    can't be decoded, the test fails loudly (as it should).
  - Write tests (supply, setUsingAsCollateral): switch from estimateGas
    to provider.call with the zero-balance TEST_ADDRESS to trigger a
    business-logic revert, and assert rejects.toMatchObject({ code:
    "CALL_EXCEPTION" }) -- ethers v6's canonical code for contract-level
    reverts. An ABI-level failure (INVALID_ARGUMENT, BAD_DATA) or an
    unknown selector raises a different error class, so the matcher
    distinguishes "calldata was understood by the contract" from
    "calldata never got that far".

Test count unchanged (6). Integration tests remain gated on
INTEGRATION_TEST_MAINNET_RPC_URL, so this change doesn't affect the
default CI path.
Running the suite against mainnet revealed that setUsingAsCollateral
silently succeeds on reserveId=0 (the Spoke no-ops on nonexistent
reserves rather than reverting), returning "0x". My previous assertion
rejects.toMatchObject({ code: "CALL_EXCEPTION" }) was too narrow --
it assumed every write would revert for a zero-balance caller.

What the write tests are actually proving is "the deployed bytecode
understood our calldata". Both a clean return ("0x" for void functions)
and CALL_EXCEPTION are valid evidence of that. What we still reject:
INVALID_ARGUMENT, BAD_DATA, BUFFER_OVERRUN -- ABI-level errors that
would signal the protocol definition doesn't match the deployed contract.

Extracted the try/catch into expectCallAcceptedByBytecode() so supply
and setUsingAsCollateral share the assertion shape.

Verified: all 6 integration tests pass against eth-mainnet.
feat: add Aave V4 protocol (and update V3)
fix: guard wallet RPC calls against undefined result and retry transients
Read-workflow calls via /api/mcp/workflows/[slug]/call now block up to 25s
for execution to finish and return the mapped output inline. Long-running
reads still degrade gracefully to {executionId, status: "running"} so
callers can poll. Write workflows are unchanged.
…crash

fix: disable auto-translation on workflow editor to prevent React reconciliation crashes
…gn-nav-reorg

feat(billing): landing-style redesign, billing details, nav reorg
fix(bazaar): emit discoverable/category/tags + fix resource URL for CDP Bazaar indexing
…audit

chore(chain-id): migrate legacy Tempo chain ID 42420 to 4217
docs: paid-workflows + agentcash-install guides for dual-chain revenue (KEEP-259 part 1)
…ings-ui

feat(earnings): per-chain revenue breakdown + docs tooltip (KEEP-259 part 2)
…hould-wait-for-read-workflow-completion

fix(mcp): make call_workflow wait for read completion (KEEP-265)
eskp and others added 2 commits April 21, 2026 17:48
The main-content branch used a fixed pt-20, so when AppBanner was
visible (fresh sessions without dismissal), the persistent
WorkflowToolbar stacked below the banner covered the AnalyticsHeader
and the time-range nav was unclickable. The hasNoData branch already
used pt-[calc(5rem+var(--app-banner-height,0px))] -- applying the
same pattern here.

Fixes the failing analytics-gas e2e test on staging.
fix(analytics): account for app-banner height in page padding
@eskp eskp merged commit 58dbe04 into prod Apr 21, 2026
24 checks 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