Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"schema_version": "agent-bounties/keeperhub-agents-onchain-canary-evidence-v1",
"recorded_at": "2026-08-13T05:43:35.057Z",
"source": {
"commit": "3937cc8360424b3d04bb65f6d25f66969c5e8392",
"pull_request": "https://github.com/NSPG13/agent-bounties/pull/932"
},
"network": {
"name": "Base Sepolia",
"chain_id": 84532,
"rpc_verification": "https://sepolia.base.org"
},
"keeperhub": {
"execution_id": "z2lp1eatpds9fx766rktg",
"organization_wallet": "0xd8688bc66c7059a84d24cd9849207b838bf72bf2",
"simulation": {
"success": true,
"would_revert": false,
"gas_estimate": "504537",
"predicted_bounty": "0x5f2df60ff6264b9c20d5da4e13f168772af57a30",
"predicted_bounty_id": "0x85d90e337ccfb788d576012b3c892dc6f0775acdb2b744268e612c7dc66e1889"
}
},
"transaction": {
"hash": "0x80fb04d83d6135c2b1f9753d9fb449a693d9f1be0a84fddcc60f03ecee6ab329",
"explorer_url": "https://sepolia.basescan.org/tx/0x80fb04d83d6135c2b1f9753d9fb449a693d9f1be0a84fddcc60f03ecee6ab329",
"status": "0x1",
"block_number": 45415763,
"block_hash": "0x4e8d87c1323db884c355afb9ed726e07eb352100d07469c568f99192784a7c98",
"gas_used": "560155",
"factory": "0x7231f1312448fa60078fb56cdb6e2c392bd1269b",
"bounty": "0x5f2df60ff6264b9c20d5da4e13f168772af57a30",
"bounty_id": "0x85d90e337ccfb788d576012b3c892dc6f0775acdb2b744268e612c7dc66e1889",
"creator": "0xd8688bc66c7059a84d24cd9849207b838bf72bf2",
"canonical_factory_registration": true,
"canonical_event": {
"name": "CanonicalCompetitionCreated",
"topic0": "0xd89805182f83e81946ee47273c3282be41947da1982d6d1788b61682be82dec5"
}
},
"economics": {
"initial_funding_usdc_units": "0",
"target_usdc_units": "110000",
"native_value_wei": "0",
"public_inventory_state": "funding_needed"
},
"evidence_boundary": "This evidence proves one KeeperHub-submitted Base Sepolia canary transaction and canonical Open Competition creation. It does not prove bounty funding, solver settlement, or payment."
}
112 changes: 112 additions & 0 deletions docs/keeperhub-execution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# KeeperHub execution adapter

Agent Bounties uses KeeperHub as a bounded onchain execution layer. The first
public integration creates one **unfunded Base Sepolia Open Competition
canary** through the rehearsed V1 factory. It spends testnet gas only, transfers
no USDC, and does not modify an existing bounty.

This integration is deliberately narrower than KeeperHub's generic direct
execution API:

- chain: Base Sepolia (`84532`)
- contract: `0x7231f1312448fa60078fb56cdb6e2c392bd1269b`
- function: `createCompetition`
- native value: zero
- initial USDC funding: zero
- verifier: `LeadingZeroWorkVerifier(16)` at
`0x9601a40b35ad6843846732c6cb73c4c82f9ba850`

The adapter rejects every other chain, contract, function, native value, and
nonzero initial-funding request.

## Judge in 60 seconds

The public KeeperHub execution is
[`0x80fb...b329`](https://sepolia.basescan.org/tx/0x80fb04d83d6135c2b1f9753d9fb449a693d9f1be0a84fddcc60f03ecee6ab329),
with KeeperHub execution ID `z2lp1eatpds9fx766rktg`. Its machine-readable
receipt is checked in at
[`docs/evidence/keeperhub-agents-onchain-canary-base-sepolia-2026-08-13.json`](evidence/keeperhub-agents-onchain-canary-base-sepolia-2026-08-13.json).

Verify the receipt directly against Base Sepolia:

```powershell
node scripts/verify_keeperhub_canary_evidence.mjs `
--evidence docs/evidence/keeperhub-agents-onchain-canary-base-sepolia-2026-08-13.json `
--rpc-url https://sepolia.base.org
```

The verifier fails closed unless the RPC reports Base Sepolia, the exact
transaction succeeded at the recorded block with the recorded gas usage, and
the factory emitted exactly one matching `CanonicalCompetitionCreated` event
for the recorded bounty ID, bounty address, and creator. It does not infer
funding or payment from a successful transaction.

## Authentication

Create an organization API key (`kh_`) in KeeperHub under **Settings → API
Keys → Organisation**. Store it only in the local `KH_API_KEY` environment
variable. Never put it in a request file, shell history, issue, receipt, commit,
or chat message.

KeeperHub's organization wallet needs a small Base Sepolia ETH balance for gas.
No USDC is needed for this canary.

## Prepare the exact request

Use the KeeperHub organization wallet shown in the KeeperHub Wallet page:

```powershell
node scripts/build_keeperhub_open_competition_canary.mjs `
--wallet 0xKEEPERHUB_ORG_WALLET `
--source-url https://github.com/NSPG13/agent-bounties/issues/931 `
--output target/keeperhub-open-competition-canary.json
```

The request commits to a 0.10 test-USDC solver reward and 0.01 test-USDC
verifier reward, but `initialFunding` is zero. The resulting bounty remains in
`funding_needed` unless it is separately funded later.

## Simulate before signing

```powershell
node scripts/keeperhub_direct_execution.mjs simulate `
--request target/keeperhub-open-competition-canary.json
```

Continue only if KeeperHub returns `success: true` and `wouldRevert: false`.
A simulation is not a transaction or payment receipt.

## Execute once and retain the receipt

Execution requires a fresh idempotency key and a new receipt path. The receipt
writer uses create-only semantics, so it cannot overwrite earlier evidence.

```powershell
$keeperhubIdempotencyKey = "agent-bounties-keeperhub-" + [guid]::NewGuid()
node scripts/keeperhub_direct_execution.mjs execute `
--request target/keeperhub-open-competition-canary.json `
--idempotency-key $keeperhubIdempotencyKey `
--receipt target/keeperhub-open-competition-receipt.json
```

The adapter polls KeeperHub's status endpoint using its poll-interval hint and
accepts success only when the final response contains all of:

- `status: completed`
- a 32-byte transaction hash
- an HTTPS block-explorer link

The public receipt deliberately excludes the API key. It proves one
KeeperHub-submitted Base Sepolia transaction. It does not prove bounty funding,
solver settlement, or payment; only canonical contract events establish those
states.

## Verification

```powershell
node scripts/test_keeperhub_direct_execution.mjs
```

The tests cover simulation enforcement, the chain/contract/function allowlist,
zero initial funding, idempotency, status polling, receipt requirements, and
secret exclusion.
138 changes: 138 additions & 0 deletions scripts/build_keeperhub_open_competition_canary.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#!/usr/bin/env node

import { createHash, randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import { pathToFileURL } from "node:url";

import {
BASE_SEPOLIA_CHAIN_ID,
BASE_SEPOLIA_OPEN_COMPETITION_FACTORY,
REQUEST_SCHEMA,
validateRequest,
} from "./keeperhub_direct_execution.mjs";

export const BASE_SEPOLIA_LEADING_ZERO_VERIFIER =
"0x9601a40b35ad6843846732c6cb73c4c82f9ba850";

function invariant(condition, message) {
if (!condition) throw new Error(message);
}

function bytes32(label) {
return `0x${createHash("sha256").update(label).digest("hex")}`;
}

function isAddress(value) {
return typeof value === "string" && /^0x[0-9a-fA-F]{40}$/.test(value);
}

export function buildCanaryRequest({ wallet, sourceUrl, nowSeconds = Math.floor(Date.now() / 1_000) }) {
invariant(isAddress(wallet), "--wallet must be an EVM address");
const parsedSource = new URL(sourceUrl);
invariant(parsedSource.protocol === "https:", "--source-url must use HTTPS");
invariant(Number.isSafeInteger(nowSeconds) && nowSeconds > 0, "nowSeconds is invalid");

const canaryId = `keeperhub-agents-onchain-${nowSeconds}-${randomUUID()}`;
const params = {
solverReward: "100000",
verifierReward: "10000",
termsHash: bytes32(`${canaryId}:terms:${sourceUrl}`),
policyHash: bytes32(`${canaryId}:policy:deterministic-first`),
acceptanceCriteriaHash: bytes32(`${canaryId}:criteria:keeperhub-receipt`),
benchmarkHash: bytes32(`${canaryId}:benchmark:leading-zero-16`),
evidenceSchemaHash: bytes32("agent-bounties/keeperhub-direct-execution-receipt-v1"),
fundingDeadline: String(nowSeconds + 7 * 24 * 60 * 60),
competitionWindowSeconds: "86400",
revealWindowSeconds: "3600",
maxEntries: 4,
verifierModule: BASE_SEPOLIA_LEADING_ZERO_VERIFIER,
verifierRewardRecipient: wallet,
};

const request = {
schema_version: REQUEST_SCHEMA,
operation: "contract_call",
chain_id: BASE_SEPOLIA_CHAIN_ID,
contract_address: BASE_SEPOLIA_OPEN_COMPETITION_FACTORY,
function_name: "createCompetition",
function_args: [params, "0", bytes32(`${canaryId}:creation-nonce`)],
abi: [
{
type: "function",
name: "createCompetition",
stateMutability: "nonpayable",
inputs: [
{
name: "params",
type: "tuple",
components: [
{ name: "solverReward", type: "uint256" },
{ name: "verifierReward", type: "uint256" },
{ name: "termsHash", type: "bytes32" },
{ name: "policyHash", type: "bytes32" },
{ name: "acceptanceCriteriaHash", type: "bytes32" },
{ name: "benchmarkHash", type: "bytes32" },
{ name: "evidenceSchemaHash", type: "bytes32" },
{ name: "fundingDeadline", type: "uint64" },
{ name: "competitionWindowSeconds", type: "uint64" },
{ name: "revealWindowSeconds", type: "uint64" },
{ name: "maxEntries", type: "uint8" },
{ name: "verifierModule", type: "address" },
{ name: "verifierRewardRecipient", type: "address" },
],
},
{ name: "initialFunding", type: "uint256" },
{ name: "creationNonce", type: "bytes32" },
],
outputs: [
{ name: "bountyAddress", type: "address" },
{ name: "bountyId", type: "bytes32" },
],
},
],
value: "0",
source_url: sourceUrl,
title: "KeeperHub execution canary — unfunded Open Competition",
expected_effect: {
event: "CanonicalCompetitionCreated",
initial_funding_usdc_units: "0",
target_usdc_units: "110000",
public_inventory_state: "funding_needed",
},
evidence_boundary:
"This request creates one new unfunded Base Sepolia canary. It moves no USDC, changes no existing bounty, and cannot prove settlement or payment.",
};
return validateRequest(request);
}

function parseCliArgs(argv) {
const options = {};
for (let index = 0; index < argv.length; index += 1) {
const key = argv[index];
invariant(key.startsWith("--"), `unexpected argument: ${key}`);
const value = argv[index + 1];
invariant(value !== undefined && !value.startsWith("--"), `missing value for ${key}`);
options[key.slice(2)] = value;
index += 1;
}
return options;
}

async function main() {
const options = parseCliArgs(process.argv.slice(2));
invariant(options.wallet, "--wallet is required");
invariant(options["source-url"], "--source-url is required");
invariant(options.output, "--output is required");
const request = buildCanaryRequest({ wallet: options.wallet, sourceUrl: options["source-url"] });
await writeFile(resolve(options.output), `${JSON.stringify(request, null, 2)}\n`, { flag: "wx" });
process.stdout.write(`${resolve(options.output)}\n`);
}

const invokedAsScript = process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href;
if (invokedAsScript) {
main().catch((error) => {
process.stderr.write(`build_keeperhub_open_competition_canary: ${error.message}\n`);
process.exitCode = 1;
});
}
2 changes: 2 additions & 0 deletions scripts/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,8 @@ def main() -> int:
["--check", "scripts/open-competition-v1-signer.js"],
["scripts/test-open-competition-v1-signer-console.js"],
["scripts/test-create-competition-flow.js"],
["scripts/test_keeperhub_direct_execution.mjs"],
["scripts/test_verify_keeperhub_canary_evidence.mjs"],
["--check", "site/standing-meta-v3-migration.js"],
)])
py("-m", "pip", "install", "-r", "scripts/requirements-attest.txt")
Expand Down
Loading
Loading