Skip to content

docs: document gas-sponsorship fallback and the insufficient_balance failure - #2268

Open
n-dlms wants to merge 2 commits into
KeeperHub:stagingfrom
n-dlms:docs/gas-sponsorship-fallback
Open

docs: document gas-sponsorship fallback and the insufficient_balance failure#2268
n-dlms wants to merge 2 commits into
KeeperHub:stagingfrom
n-dlms:docs/gas-sponsorship-fallback

Conversation

@n-dlms

@n-dlms n-dlms commented Sep 2, 2026

Copy link
Copy Markdown

Summary

Sponsorship falls back silently to direct signing whenever an eligibility condition is not met — gas credits exhausted, unsupported network, Safe sender, private mempool route, or a blocked RPC. The operator only sees the downstream failure:

Insufficient ETH balance. Have: 0.0, Need: 0.000000231. Fund
0x26833b05be49036d4de306b1f4fba7713cc84de5 with at least 0.000000231 ETH
on this chain and retry.

(code insufficient_balance, from lib/execute/native-balance.ts describeNativeShortfall) — and nothing in the docs connected that error to the fallback. First-hand hit during a live integration on Base Sepolia.

Changes

  • docs/wallet-management/gas.md: new When sponsorship falls back section — the fallback conditions, the two observable outcomes (wallet-pays success vs the insufficient_balance failure), and both remedies (fund the named address / restore sponsorship conditions)
  • docs/keeper-runs/error-codes.md: document the structured insufficient_balance action failure (not a PREFIX-NNNN code), its sponsorship-fallback cause, and the fix
  • docs/keeper-runs/troubleshooting.md: cross-link from Transaction Failures

Verification

  • Behavior read from lib/web3/sponsored-transaction-manager.ts (executeSponsoredTransaction returns null on every ineligible condition, triggering the direct-signing fallback) and lib/execute/native-balance.ts (describeNativeShortfall message + insufficient_balance code)
  • Verified against staging commit d249519c5d7dcf66b8405e065cd7988a647ac442
  • Reproduced live on Base Sepolia: a funded-position workflow failed with exactly this error until the named address was funded

Docs-only change; no behavior touched. Submitted to the KeeperHub — The Agent Economy hackathon bounty (DoraHacks) as a separate BUIDL.

…failure

Sponsorship falls back silently to direct signing when an eligibility
condition is not met (credits exhausted, unsupported network, Safe sender,
private mempool, blocked RPC). Operators only see the downstream
"Insufficient <SYMBOL> balance ... Fund <address> ..." failure (code
insufficient_balance) and nothing in the docs connected that error to the
fallback.

- gas.md: add "When sponsorship falls back" (conditions, observable
  outcome, the named-address error, both remedies)
- error-codes.md: document the structured insufficient_balance action
  failure and its sponsorship-fallback cause
- troubleshooting.md: cross-link from Transaction Failures

Verified against the behavior in lib/web3/sponsored-transaction-manager.ts
(executeSponsoredTransaction returns null on every ineligible condition,
 triggering the direct-signing fallback) and lib/execute/native-balance.ts
(describeNativeShortfall message and insufficient_balance code) at commit
d249519 (staging).

@suisuss suisuss left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Welcome, and thanks for this - the contributing guide is in CONTRIBUTING.md, and ISSUES.md covers when a change needs an issue first.

What this changes

Three docs, additions only. docs/keeper-runs/error-codes.md gains a section for action failures carrying structured codes, with insufficient_balance as its worked example. docs/keeper-runs/troubleshooting.md gains a callout under Transaction Failures reading the Insufficient ETH balance message as a sponsorship fallback. docs/wallet-management/gas.md gains a "When sponsorship falls back" subsection with a sample error block and remediation.

Does it match the description

Matches. Three docs, no code, and the description says so.

Blocking

  • docs/keeper-runs/error-codes.md:41-44, docs/wallet-management/gas.md:116, docs/keeper-runs/troubleshooting.md:78-79 - a run does not carry the insufficient_balance code. describeNativeShortfall builds { code: INSUFFICIENT_BALANCE_CODE, message } (lib/execute/native-balance.ts:82), but preflightGasBalance returns { affordable: false, message: shortfall.message } (lib/web3/gas-preflight.ts:122) and discards the code, and write-contract-core.ts:585-586 then returns { success: false, error: gasCheck.message }. The code survives only on the simulate path, through SimulateFailureCode in lib/execute/simulate.ts, which is what docs/api/direct-execution.md and docs/agent/mcp-server.md already document. -> An integrator reads /keeper-runs/error-codes, branches a run webhook on code === "insufficient_balance", and it never fires; a user searches the Runs panel for that string and finds nothing. -> Scope the claim to /api/execute/* simulate responses and MCP simulate results, and key the run-side guidance on the message text.

  • docs/wallet-management/gas.md:104-106 - "Nothing in the run output tells you sponsorship was skipped" is the opposite of what ships. workflow-runs.tsx:874-876 renders a "Gas sponsored" badge whenever a step output carries sponsored: true, and runs-filters.tsx:274 offers a Sponsored run filter. -> A user reads this, does not check the run, and opens a support ticket the missing badge would have answered. -> "The run output does not say why sponsorship was skipped - a fallback run simply lacks the Gas sponsored badge."

  • docs/keeper-runs/error-codes.md:46-47 - "did not hold at broadcast time" contradicts the code and gas.md:107. preflightGasBalance runs before withNonceSession (write-contract-core.ts:579-587), so an unaffordable wallet never signs and nothing is broadcast. -> A user searches an explorer for a transaction hash that was never created. -> "before the transaction was broadcast".

  • docs/keeper-runs/troubleshooting.md:75-77 - the message does not imply a sponsorship fallback. gas-preflight.ts has no sponsorship condition in it; the Insufficient ETH balance ... Fund <address> string is emitted on every direct-signing write path, including orgs with sponsorship disabled, Safe senders, and chains that were never sponsorship-eligible. -> A Safe-routed workflow on an unsupported chain fails with this message, the reader goes hunting for exhausted gas credits, and the actual answer is to fund the Safe. -> Invert it: on a sponsorship-eligible network this message additionally means sponsorship fell back; otherwise it means the funding address cannot cover gas.

  • docs/keeper-runs/error-codes.md:59-62, docs/wallet-management/gas.md:118-120 - the four listed conditions are not sufficient to restore sponsorship. executeSponsoredTransaction also requires createSponsoredClient to return non-null (sponsored-transaction-manager.ts:119-126), and that returns null when the org's active wallet has turnkeySubOrgId === null (sponsored-client.ts:56-58). -> An org on a self-imported wallet, on Base, with credits remaining, satisfies all four listed conditions, retries expecting sponsorship, and pays gas again. -> Either say the list is the user-controllable subset and sponsorship may still be unavailable per organization and wallet, or add the Turnkey-managed-wallet condition.

Mechanical - actionable as-is

  • docs/wallet-management/gas.md:112 - the sample block embeds 0x26833b05be49036d4de306b1f4fba7713cc84de5. Every other example in these docs uses a placeholder (docs/api/direct-execution.md:625 uses 0x...orgWallet). Use a placeholder here too.
  • docs/keeper-runs/error-codes.md:42 - "The most common one on sponsored-capable networks" is a frequency claim with nothing behind it. Drop it or cite the source.

Verdict

Changes requested - the insufficient_balance code is not present on the run path these pages describe, and three of the other four claims contradict what the code does.

@suisuss suisuss added the changes-requested Triage: reviewed, changes needed from the contributor label Sep 3, 2026
…es, correct fallback visibility and conditions

- error-codes.md: the insufficient_balance code survives only on the
  simulate surface (/api/execute simulate, MCP simulate); run steps carry
  the plain message, so run-side guidance keys on message text
- gas.md: a fallback run simply lacks the Gas sponsored badge
  (workflow-runs.tsx); reword the no-broadcast claim (gas preflight runs
  before signing); add the Turnkey-managed wallet condition and note the
  eligibility list is the user-controllable subset
  (sponsored-client.ts turns self-imported wallets away)
- troubleshooting.md: the message is emitted on every direct-signing write
  path; on sponsorship-eligible networks it additionally means fallback
- replace the real wallet address in the sample with a placeholder; drop
  the unsourced frequency claim
@n-dlms

n-dlms commented Sep 3, 2026

Copy link
Copy Markdown
Author

Thank you for the review — every point checked out against the code, and the PR is updated (a417e00). Point by point:

  1. insufficient_balance is simulate-path only — fixed. error-codes.md now scopes the code to /api/execute/* simulate responses and MCP simulate results, and keys the run-side guidance on the message text, as the run path only carries the plain message (write-contract-core.ts returns error: gasCheck.message).

  2. The Gas sponsored badge exists — fixed. Reworded to: a fallback run simply lacks the Gas sponsored badge; the run output does not say why sponsorship was skipped.

  3. "did not hold at broadcast time" was wrong — fixed. preflightGasBalance runs before signing, so the docs now say the preflight stops the transaction before broadcast and there is no transaction hash to look up.

  4. The message does not imply a fallback — fixed by inversion: the message is emitted on every direct-signing write path; on a sponsorship-eligible network it additionally means sponsorship fell back.

  5. Conditions were not sufficient — fixed. Added the Turnkey-managed wallet requirement (sponsored-client.ts returns null for turnkeySubOrgId === null) and stated the list is the user-controllable subset; Turnkey can still reject at submission.

  6. Mechanical — sample address replaced with a placeholder, frequency claim dropped.

@suisuss suisuss left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What this changes

error-codes.md scopes insufficient_balance to simulate responses and MCP simulate results, and says run steps carry the plain message with no code. troubleshooting.md inverts its callout so the message reads as the preflight stopping the transaction, with sponsorship fallback as the additional reading on an eligible network. gas.md rewords the fallback trigger, adds the Gas-sponsored badge, and adds a caveat paragraph on Turnkey-managed versus self-imported wallets.

Does it match the description

Matches. Three files, all under docs/, nothing in code or CI. The specs/api-coverage.json gate does not apply - scripts/check-api-docs-routes.ts only parses docs/api/*.md.

Previously raised

  • Scope the code to the simulate surface - partially addressed; the scoping is right and the explanation attached to it is not, see the first blocker.
  • The Gas-sponsored badge - addressed.
  • "did not hold at broadcast time" - addressed; preflightGasBalance at plugins/web3/steps/write-contract-core.ts:579 does precede withNonceSession at :589.
  • Invert the fallback implication - addressed, then over-generalised, see the mechanical items.
  • Four eligibility conditions are not the whole set - partially addressed; the new caveat lands but the reworded trigger above it now contradicts the list, see the third blocker.
  • Address placeholder and the frequency claim - addressed.

Blocking

  • docs/keeper-runs/error-codes.md:47-50 - the section is titled for simulate responses but explains the gas preflight. The simulate code comes from nativeShortfallFailure, which compares the balance against the transfer value alone: lib/execute/native-balance.ts:17-19 states "Neither caller adds gas to the requirement", and lib/execute/simulate.ts:314-316 returns null for value <= 0. An integrator whose zero-value write-contract simulate fails on an empty wallet gets a revert message and never this code, and concludes the page describes a different endpoint. Say that the funding address could not cover the native value the call sends, that the simulator adds no gas term, and that a zero-value call never produces the code.

  • docs/keeper-runs/error-codes.md:64-67 - "restoring the sponsorship conditions also fixes the run without funding" is false for any value-bearing write, and contradicts gas.md:76-78 in this same diff, which says sponsorship pays the fee only and native value is always debited from the user's own wallet. The preflight requires valueWei + MIN_TX_GAS_UNITS * gasPrice (lib/web3/gas-preflight.ts:111). A user with an empty wallet and a 0.1 ETH transfer step tops up gas credits, runs again, and fails again, on real funds. Qualify it to writes that send no native value.

  • docs/wallet-management/gas.md:99-100 - "falls back when the attempt returns no sponsored client" is narrower than the code and contradicts the eligibility list above it. executeSponsoredTransaction returns null at six points: feature flag (lib/web3/sponsored-transaction-manager.ts:102), blocked RPC (:106), unsupported chain (:110), exhausted credits (:114-117), no client (:124), and a null Turnkey submit (:137). A reader whose credits ran out hunts the wrong cause. Restore "whenever any eligibility condition above is not met" and keep the per-org caveat as a separate sentence.

  • docs/wallet-management/gas.md:123-127 - the self-imported-wallet case is mine and it was wrong. createSponsoredClient does null-return on a null turnkeySubOrgId, but no org can reach that state: the only production insert into organizationWallets is lib/turnkey/provision-org-wallet.ts:103-110, which always sets turnkeySubOrgId from the Turnkey result, and there is no wallet-import route. Documenting it sends a reader looking for a wallet-type setting that does not exist instead of checking credits. Drop the parenthetical and keep only that Turnkey can reject an activity at submission time.

Mechanical - actionable as-is

  • docs/keeper-runs/error-codes.md:43 - SimulateFailureCode is an internal type alias (lib/execute/simulate.ts:102); the wire field is code, and MCP surfaces it as a Reason code: line. Name the field.
  • docs/keeper-runs/error-codes.md:59 and docs/wallet-management/gas.md:118 - "every direct-signing write path" overstates it. Six action cores call preflightGasBalance; lib/safe/deployment.ts, lib/safe/roles-orchestrator.ts and app/api/user/wallet/withdraw/route.ts sign directly and do not. "Every web3 write action" is accurate.
  • docs/wallet-management/gas.md:100-101 - the badge renders per step, so a run with one sponsored and one fallback step still shows it. The run-level Sponsored filter is at components/analytics/runs-filters.tsx:276.

Verdict

Changes requested - the scoping is right, but the explanation now attached to it describes the preflight rather than the simulator, and two statements would send someone with real funds to the wrong remedy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changes-requested Triage: reviewed, changes needed from the contributor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants