@jumboo/create-agent 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,7 +14,11 @@ It asks a few questions, then:
14
14
  - fills in the **network RPC, oracle URL, and Jumboo contract addresses**
15
15
  - scaffolds a working `POST /solve` backend built on
16
16
  [`@jumboo/agent-sdk`](https://www.npmjs.com/package/@jumboo/agent-sdk)
17
- - includes `npm run register` to mint the agent NFT on-chain
17
+
18
+ You **register the agent in the Jumboo frontend** ([jumboo.xyz](https://jumboo.xyz)
19
+ → Create Agent) using the generated hot wallet — the frontend also drives
20
+ Compete/Hire, which signs with your wallet, checks you own the agent, and calls
21
+ your backend's `/solve`. This scaffold is just that backend.
18
22
 
19
23
  ## Prompts
20
24
 
@@ -23,9 +27,7 @@ It asks a few questions, then:
23
27
  | Project directory | `my-jumboo-agent` | also the package name |
24
28
  | Network | `sepolia` | `sepolia` (live Jumboo) or `localhost` |
25
29
  | Solver | `echo` | `echo` (test), `claude` / `codex` / `opencode` / `antigravity` (AI agents), or `custom` (write your own in src/solver.js) |
26
- | Skills | `javascript, bugfix` | written into the agent metadata |
27
30
  | Hire price (USDC) | `0.50` | quoted to task creators via x402 |
28
- | Public backend URL | — | where `/solve` will be hosted |
29
31
 
30
32
  ## What you get
31
33
 
@@ -42,8 +44,6 @@ my-agent/
42
44
  github.js clone / issue / fork / PR helpers
43
45
  solver.js pluggable drivers: echo, claude/codex/opencode/antigravity, custom
44
46
  job.js clone → solve → PR → claim pipeline (uses the SDK)
45
- scripts/
46
- register.js mint the agent NFT (npm run register)
47
47
  ```
48
48
 
49
49
  ## After scaffolding
@@ -51,11 +51,13 @@ my-agent/
51
51
  ```bash
52
52
  cd my-agent
53
53
  npm install
54
- # set OPERATOR_KEY in .env, fund it, then:
55
- npm run register # prints AGENT_ID — copy it into .env
56
- npm start # DRY_RUN=1 by default
54
+ npm start # boots now with echo + DRY_RUN; GET /health works
57
55
  ```
58
56
 
57
+ To compete for real: register the agent at [jumboo.xyz](https://jumboo.xyz) with
58
+ the generated hot wallet, paste the `AGENT_ID` into `.env`, pick a real `SOLVER`,
59
+ fund the hot wallet, and set `DRY_RUN=0`.
60
+
59
61
  ## License
60
62
 
61
63
  MIT
package/bin/index.js CHANGED
@@ -7,8 +7,7 @@
7
7
  *
8
8
  * Interactive in a terminal; also fully scriptable with flags + --yes:
9
9
  * npx @jumboo/create-agent my-agent --yes \
10
- * --network sepolia --solver echo --skills "javascript,bugfix" \
11
- * --price 0.50 --backend https://my-agent.example.com
10
+ * --network sepolia --solver echo --price 0.50
12
11
  *
13
12
  * Asks a few questions, generates a fresh hot wallet, fills in the network +
14
13
  * contract addresses, and writes a working /solve backend that uses
@@ -97,11 +96,9 @@ async function main() {
97
96
  solver = "echo";
98
97
  }
99
98
 
100
- const skills = await resolve(args.skills, "Skills (comma-separated):", "javascript, bugfix");
101
99
  const hirePrice = await resolve(args.price, "Hire price in USDC (x402):", "0.50");
102
- const backendUrl = await resolve(args.backend, "Public backend URL (where /solve will be hosted):", "https://your-agent.example.com");
103
100
 
104
- // -- Generate a fresh hot wallet ---------------------------------------
101
+ // -- Generate a fresh master mnemonic (index-0 address = its fingerprint) --
105
102
  const wallet = ethers.Wallet.createRandom();
106
103
 
107
104
  // -- Scaffold the project ----------------------------------------------
@@ -110,22 +107,18 @@ async function main() {
110
107
  await copyTemplate(TEMPLATE_DIR, targetDir, { __PROJECT_NAME__: projectName });
111
108
 
112
109
  // -- Write the generated .env (never a template — it holds the key) -----
113
- const skillsJson = JSON.stringify(skills.split(",").map((s) => s.trim()).filter(Boolean));
114
110
  const env = [
115
111
  `# ${projectName} — generated by @jumboo/create-agent. Never commit this file.`,
116
112
  "",
117
113
  "PORT=8917",
118
114
  `RPC_URL=${network.rpcUrl}`,
119
115
  "",
120
- "# This agent's on-chain NFT id. Filled in by `npm run register`.",
121
- "AGENT_ID=",
122
- "",
123
- "# Freshly generated hot wallet signs task markers. Keep it secret.",
124
- `AGENT_HOT_KEY=${wallet.privateKey}`,
125
- "# Wallet that pays gas for the claim tx (defaults to the hot key).",
116
+ "# Freshly generated master mnemonic ONE phrase runs ALL your agents.",
117
+ "# Each agent's hot wallet is HD-derived from it. Keep it secret.",
118
+ `AGENT_MASTER_MNEMONIC=${wallet.mnemonic.phrase}`,
119
+ "# Optional funded wallet that pays claim gas for every agent (else each",
120
+ "# agent's own derived hot wallet pays):",
126
121
  "AGENT_TX_KEY=",
127
- "# Operator wallet that OWNS the agent NFT + runs `npm run register` (fund it first).",
128
- "OPERATOR_KEY=",
129
122
  "",
130
123
  "# GitHub machine user (clone auth, issue fetch, fork, PR). Empty for DRY_RUN.",
131
124
  "GITHUB_TOKEN=",
@@ -149,11 +142,6 @@ async function main() {
149
142
  "# SOLVER_PROMPT_VIA=stdin # stdin | arg",
150
143
  ]),
151
144
  "",
152
- "# Agent identity metadata (used by `npm run register`).",
153
- `AGENT_NAME=${projectName}`,
154
- `AGENT_SKILLS=${skillsJson}`,
155
- `BACKEND_URL=${backendUrl}`,
156
- "",
157
145
  "# x402 hire price quoted to task creators.",
158
146
  `HIRE_PRICE_AMOUNT=${hirePrice}`,
159
147
  "HIRE_PRICE_ASSET=USDC",
@@ -170,9 +158,8 @@ async function main() {
170
158
  console.log(`
171
159
  ✓ Created ${projectName} (${network.label})
172
160
 
173
- A fresh hot wallet was generated:
174
- address: ${wallet.address}
175
- (private key saved to .env — keep it secret, it is gitignored)
161
+ A fresh master mnemonic was generated (index-0 address ${wallet.address}).
162
+ Saved to .env — keep it secret (gitignored). All your agents derive from it.
176
163
 
177
164
  Get it running now:
178
165
  1. cd ${targetArg}
@@ -180,9 +167,10 @@ async function main() {
180
167
  3. npm start # echo solver + DRY_RUN — GET /health works
181
168
 
182
169
  To compete for real:
170
+ - register agents at https://jumboo.xyz (Create Agent) — the frontend derives
171
+ each agent's hot wallet from this master; this backend serves them all
183
172
  - pick a solver in .env (SOLVER=claude / codex / opencode / antigravity)
184
- - set OPERATOR_KEY, fund it, then: npm run register # writes AGENT_ID
185
- - fund the hot wallet ${wallet.address} (claim gas), then set DRY_RUN=0
173
+ - fund the gas wallet, then set DRY_RUN=0
186
174
 
187
175
  Docs: https://docs.jumboo.xyz/#/build-a-jumboo-agent
188
176
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jumboo/create-agent",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Scaffold a ready-to-run Jumboo agent: npm create @jumboo/agent my-agent",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,25 +16,29 @@ npm start
16
16
 
17
17
  That's it — open <http://localhost:8917/health> and the agent is running. Out of
18
18
  the box it uses the `echo` solver (no AI) with `DRY_RUN=1`, so the whole pipeline
19
- works with no keys and no registration. A fresh hot wallet was already generated
20
- into `.env`.
19
+ works with no registration. A fresh **master mnemonic** was already generated
20
+ into `.env` — every agent you register derives from it.
21
21
 
22
22
  ### Going live (to compete for real)
23
23
 
24
- 1. Pick a real solver in `.env`: `SOLVER=claude` (or `codex` / `opencode` /
24
+ 1. **Register your agent(s)** at [jumboo.xyz](https://jumboo.xyz) **Create
25
+ Agent**. Each agent's hot wallet is HD-derived from your `AGENT_MASTER_MNEMONIC`
26
+ (the frontend derives the address to register). This one backend then serves
27
+ **all** of them — nothing to paste per agent. Registration and the Compete/Hire
28
+ buttons all live in the frontend, which calls this backend's `/solve` with the
29
+ chosen `agentId`.
30
+ 2. Pick a real solver in `.env`: `SOLVER=claude` (or `codex` / `opencode` /
25
31
  `antigravity`) — that tool must be installed on the host.
26
- 2. **Register the agent** (mints your NFT, prints `AGENT_ID`):
27
- - Fund the OPERATOR wallet, set `OPERATOR_KEY` in `.env`, then `npm run register`.
28
- - Copy the printed `AGENT_ID=...` into `.env`.
29
- 3. Fund the hot wallet (shown at scaffold time) with a little ETH for claim gas.
32
+ 3. Fund the claim-gas wallet with a little ETH (`AGENT_TX_KEY`, or each agent's
33
+ own derived wallet).
30
34
  4. Set `DRY_RUN=0` and `npm start`.
31
35
 
32
36
  ## Endpoints
33
37
 
34
38
  | Method | Path | Description |
35
39
  |---|---|---|
36
- | `GET` | `/health` | Agent id, hot wallet, solver, job count |
37
- | `POST` | `/solve` | Body `{ "taskId": "0x<64 hex>" }` + auth headers → `202 { jobId }` |
40
+ | `GET` | `/health` | Master fingerprint, solver, job count |
41
+ | `POST` | `/solve` | Body `{ "taskId": "0x<64 hex>", "agentId": <n> }` + auth headers → `202 { jobId }` |
38
42
  | `GET` | `/jobs/:id` | Job status |
39
43
 
40
44
  `POST /solve` requires caller-auth headers (`X-Jumboo-Address`,
@@ -4,15 +4,14 @@
4
4
  PORT=8917
5
5
  RPC_URL=https://ethereum-sepolia-rpc.publicnode.com
6
6
 
7
- # This agent's on-chain NFT id (filled in by `npm run register`).
8
- AGENT_ID=
7
+ # Master mnemonic ONE phrase runs ALL your agents. Each agent's hot wallet is
8
+ # HD-derived from it. Register agents in the Jumboo frontend using the derived
9
+ # addresses; this backend then serves every one of them. Keep this secret.
10
+ AGENT_MASTER_MNEMONIC=
9
11
 
10
- # The agent's registered hot wallet signs task markers. Keep secret.
11
- AGENT_HOT_KEY=
12
- # Optional separate wallet that pays gas for the claim tx (defaults to hot key).
12
+ # Optional funded wallet that pays claim gas for every agent. When blank, each
13
+ # agent's own derived hot wallet pays (so fund those instead).
13
14
  AGENT_TX_KEY=
14
- # Operator wallet that owns the agent NFT + runs `npm run register`.
15
- OPERATOR_KEY=
16
15
 
17
16
  # GitHub machine user (clone auth, issue fetch, fork, PR). Empty for DRY_RUN.
18
17
  GITHUB_TOKEN=
@@ -30,7 +29,7 @@ WORKDIR=./workspace
30
29
  # claude Claude Code CLI (needs `claude` installed + logged in)
31
30
  # codex OpenAI Codex CLI (needs `codex`)
32
31
  # opencode opencode CLI (needs `opencode`)
33
- # antigravity Antigravity CLI (needs `antigravity`)
32
+ # antigravity Antigravity CLI (needs `agy`)
34
33
  # custom write your own driver — implement solve() in src/solver.js
35
34
  SOLVER=echo
36
35
 
@@ -40,11 +39,6 @@ SOLVER=echo
40
39
  # SOLVER_ARGS=-p --dangerously-skip-permissions
41
40
  # SOLVER_PROMPT_VIA=stdin # "stdin" or "arg"
42
41
 
43
- # Agent identity metadata (used by `npm run register`).
44
- AGENT_NAME=__PROJECT_NAME__
45
- AGENT_SKILLS=["javascript","bugfix"]
46
- BACKEND_URL=https://your-agent.example.com
47
-
48
42
  # x402 hire price quoted to task creators.
49
43
  HIRE_PRICE_AMOUNT=0.50
50
44
  HIRE_PRICE_ASSET=USDC
@@ -6,11 +6,10 @@
6
6
  "type": "module",
7
7
  "scripts": {
8
8
  "start": "node src/index.js",
9
- "dev": "node --watch src/index.js",
10
- "register": "node scripts/register.js"
9
+ "dev": "node --watch src/index.js"
11
10
  },
12
11
  "dependencies": {
13
- "@jumboo/agent-sdk": "^0.1.0",
12
+ "@jumboo/agent-sdk": "^0.2.0",
14
13
  "dotenv": "^16.4.5",
15
14
  "ethers": "^6.13.0",
16
15
  "express": "^4.19.2"
@@ -33,7 +33,7 @@ export function verifyCaller(req, taskId) {
33
33
  }
34
34
 
35
35
  /** The authoritative x402 payment requirements for one /solve attempt. */
36
- export function paymentRequirements({ taskId, operatorWallet }) {
36
+ export function paymentRequirements({ taskId, agentId, operatorWallet }) {
37
37
  return {
38
38
  x402Version: 1,
39
39
  accepts: [
@@ -44,7 +44,7 @@ export function paymentRequirements({ taskId, operatorWallet }) {
44
44
  amount: config.hirePrice.amount,
45
45
  payTo: operatorWallet,
46
46
  resource: "/solve",
47
- description: `Hire agent #${config.agentId} to attempt task ${taskId}`,
47
+ description: `Hire agent #${agentId} to attempt task ${taskId}`,
48
48
  },
49
49
  ],
50
50
  };
@@ -87,7 +87,7 @@ export function verifyPayment(req, requirements) {
87
87
  * The full access decision for POST /solve.
88
88
  * Returns { allow: true, path } or { allow: false, status, body }.
89
89
  */
90
- export function authorizeSolve({ req, caller, agent, task, taskId }) {
90
+ export function authorizeSolve({ req, caller, agent, task, taskId, agentId }) {
91
91
  // 1. Operator triggers its own agent → free (Compete).
92
92
  if (caller.toLowerCase() === agent.operatorWallet.toLowerCase()) {
93
93
  return { allow: true, path: "compete" };
@@ -95,7 +95,7 @@ export function authorizeSolve({ req, caller, agent, task, taskId }) {
95
95
 
96
96
  // 2. Task creator hires this agent → x402 handshake (Hire).
97
97
  if (caller.toLowerCase() === task.creator.toLowerCase()) {
98
- const requirements = paymentRequirements({ taskId, operatorWallet: agent.operatorWallet });
98
+ const requirements = paymentRequirements({ taskId, agentId, operatorWallet: agent.operatorWallet });
99
99
  const payment = verifyPayment(req, requirements);
100
100
  if (!payment.ok) {
101
101
  return {
@@ -1,14 +1,19 @@
1
1
  /**
2
- * On-chain reads: load a task and the agent identity. The protocol-critical
3
- * writes (signing the marker, claiming the escrow) live in @jumboo/agent-sdk.
2
+ * On-chain reads + deriving the right hot wallet for an agent.
3
+ *
4
+ * This backend runs MANY agents from one master mnemonic (config.masterMnemonic).
5
+ * Each agent's hot wallet is HD-derived from the master at the per-agent index
6
+ * stored in its ERC-8004 metadata (`hotWalletIndex`). The marker signing + escrow
7
+ * claim themselves live in @jumboo/agent-sdk.
4
8
  */
5
9
  import { ethers } from "ethers";
10
+ import { deriveHotWallet } from "@jumboo/agent-sdk";
6
11
  import { config } from "./config.js";
7
12
 
8
13
  export const provider = new ethers.JsonRpcProvider(config.rpcUrl);
9
14
 
10
- /** The agent's registered hot wallet — signs taskIds for PR markers. */
11
- export const hotWallet = new ethers.Wallet(config.agentHotKey);
15
+ /** Address of the master's index-0 wallet — a public fingerprint, not a key. */
16
+ export const masterFingerprint = deriveHotWallet(config.masterMnemonic, 0).address;
12
17
 
13
18
  // TaskState enum in JumbooTaskRegistry.sol
14
19
  export const TaskState = {
@@ -35,8 +40,8 @@ export async function getTask(taskId) {
35
40
  return task;
36
41
  }
37
42
 
38
- /** Loads this backend's agent identity; returns null when not registered. */
39
- export async function getAgent(agentId = config.agentId) {
43
+ /** Loads an agent's identity; returns null when it does not exist. */
44
+ export async function getAgent(agentId) {
40
45
  try {
41
46
  const [operatorWallet, agentWallet, agentURI, active] = await identityRegistry.getAgent(agentId);
42
47
  return { operatorWallet, agentWallet, agentURI, active };
@@ -44,3 +49,36 @@ export async function getAgent(agentId = config.agentId) {
44
49
  return null;
45
50
  }
46
51
  }
52
+
53
+ /** Read the per-agent hot-wallet index out of the ERC-8004 data-URI metadata. */
54
+ function hotWalletIndexFromUri(agentURI) {
55
+ try {
56
+ const m = String(agentURI).match(/^data:application\/json;base64,(.+)$/);
57
+ if (!m) return null;
58
+ const json = JSON.parse(Buffer.from(m[1], "base64").toString("utf8"));
59
+ return Number.isInteger(json.hotWalletIndex) ? json.hotWalletIndex : null;
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Load an agent AND derive its hot wallet from our master mnemonic.
67
+ *
68
+ * Returns { agent, index, hotWallet, ownedByUs } or null when the agent does not
69
+ * exist. `ownedByUs` is true only when the derived wallet matches the agent's
70
+ * registered `agentWallet` — i.e. this agent was registered from OUR master, so
71
+ * we're allowed to sign for it. This is the trustless ownership check.
72
+ */
73
+ export async function getAgentWithSigner(agentId) {
74
+ const agent = await getAgent(agentId);
75
+ if (!agent) return null;
76
+ const index = hotWalletIndexFromUri(agent.agentURI);
77
+ let hotWallet = null;
78
+ let ownedByUs = false;
79
+ if (index !== null) {
80
+ hotWallet = deriveHotWallet(config.masterMnemonic, index);
81
+ ownedByUs = hotWallet.address.toLowerCase() === agent.agentWallet.toLowerCase();
82
+ }
83
+ return { agent, index, hotWallet, ownedByUs };
84
+ }
@@ -29,26 +29,24 @@ function intEnv(name, fallback) {
29
29
  return n;
30
30
  }
31
31
 
32
- // Optional so `npm start` works right after scaffolding, before you register.
33
- // Defaults to "0" (unregistered): the server boots and /health responds; you
34
- // need a real id (from `npm run register`) before the agent can win a task.
35
- const agentId = process.env.AGENT_ID || "0";
36
- if (!/^\d+$/.test(agentId)) throw new Error(`AGENT_ID must be a non-negative integer, got: ${agentId}`);
37
-
38
32
  const solver = process.env.SOLVER || "echo";
39
33
  if (!SOLVERS.includes(solver)) {
40
34
  throw new Error(`SOLVER must be one of ${SOLVERS.join(" | ")}, got: ${solver}`);
41
35
  }
42
36
 
43
- const hotKey = required("AGENT_HOT_KEY", "the agent's registered hot wallet private key");
44
- if (!/^0x[0-9a-fA-F]{64}$/.test(hotKey)) throw new Error("AGENT_HOT_KEY must be a 0x-prefixed 32-byte hex private key");
37
+ // One master mnemonic runs ALL of this operator's agents: each agent's hot
38
+ // wallet is HD-derived from it (see chain.js + @jumboo/agent-sdk). You never
39
+ // paste a wallet per agent — register more agents in the frontend and this
40
+ // backend serves them automatically.
41
+ const masterMnemonic = required("AGENT_MASTER_MNEMONIC", "the operator's 12/24-word HD master phrase");
45
42
 
46
43
  export const config = {
47
44
  port: intEnv("PORT", 8917),
48
45
  rpcUrl: required("RPC_URL"),
49
- agentId,
50
- agentHotKey: hotKey,
51
- agentTxKey: process.env.AGENT_TX_KEY || hotKey,
46
+ masterMnemonic,
47
+ // Optional funded wallet that pays the claim gas for every agent. When blank,
48
+ // each agent's own derived hot wallet pays (so it must hold a little ETH).
49
+ agentTxKey: process.env.AGENT_TX_KEY || "",
52
50
  githubToken: process.env.GITHUB_TOKEN || "",
53
51
  githubUsername: process.env.GITHUB_USERNAME || "jumboo-agent",
54
52
  oracleUrl: required("ORACLE_URL", "e.g. https://oracle.jumboo.xyz").replace(/\/+$/, ""),
@@ -1,13 +1,17 @@
1
1
  /**
2
2
  * __PROJECT_NAME__ — a Jumboo agent backend.
3
3
  *
4
- * Receives POST /solve, enforces the Compete/Hire (x402) access policy, then
5
- * runs the full pipeline: clone repo solve issue open PR with the Jumboo
6
- * marker block poll the oracleauto-claim the escrow (via @jumboo/agent-sdk).
4
+ * Receives POST /solve { taskId, agentId }, enforces the Compete/Hire (x402)
5
+ * access policy, derives the agent's hot wallet from this backend's master
6
+ * mnemonic, then runs the pipeline: clone repo solve issue open PR with the
7
+ * Jumboo marker → poll the oracle → auto-claim the escrow (via @jumboo/agent-sdk).
8
+ *
9
+ * One backend serves every agent registered from its master — the frontend says
10
+ * which agentId to compete as; nothing to configure per agent.
7
11
  */
8
12
  import express from "express";
9
13
  import { config } from "./config.js";
10
- import { hotWallet, getAgent, getTask, TaskState } from "./chain.js";
14
+ import { getTask, TaskState, getAgentWithSigner, masterFingerprint } from "./chain.js";
11
15
  import { verifyCaller, authorizeSolve } from "./auth.js";
12
16
  import { startJob, getJob, jobView, jobCount } from "./job.js";
13
17
 
@@ -15,10 +19,13 @@ const app = express();
15
19
  app.use(express.json());
16
20
 
17
21
  app.get("/health", (_req, res) => {
22
+ // Public read-only status — allow the Jumboo frontend to verify the master
23
+ // fingerprint cross-origin when you register agents.
24
+ res.set("Access-Control-Allow-Origin", "*");
18
25
  res.json({
19
26
  ok: true,
20
- agentId: config.agentId,
21
- wallet: hotWallet.address,
27
+ mode: "master",
28
+ master: masterFingerprint, // address of derivation index 0 (identifies the master)
22
29
  solver: config.solver,
23
30
  dryRun: config.dryRun,
24
31
  jobs: jobCount(),
@@ -27,11 +34,17 @@ app.get("/health", (_req, res) => {
27
34
 
28
35
  app.post("/solve", async (req, res) => {
29
36
  try {
37
+ // -- Request shape ------------------------------------------------------
30
38
  const taskId = req.body?.taskId;
31
39
  if (!/^0x[0-9a-fA-F]{64}$/.test(taskId || "")) {
32
- return res.status(400).json({ error: "body must be { taskId: '0x<64 hex>' }" });
40
+ return res.status(400).json({ error: "body must include taskId: '0x<64 hex>'" });
41
+ }
42
+ const agentId = req.body?.agentId;
43
+ if (!/^\d+$/.test(String(agentId ?? ""))) {
44
+ return res.status(400).json({ error: "body must include agentId (the agent to compete as)" });
33
45
  }
34
46
 
47
+ // -- Caller authentication (EIP-191 over `jumboo-solve:<taskId>`) --------
35
48
  const caller = verifyCaller(req, taskId);
36
49
  if (!caller) {
37
50
  return res.status(401).json({
@@ -41,14 +54,23 @@ app.post("/solve", async (req, res) => {
41
54
  });
42
55
  }
43
56
 
44
- const agent = await getAgent(config.agentId);
45
- if (!agent) {
46
- return res.status(500).json({ error: `agent #${config.agentId} is not registered on-chain` });
57
+ // -- Resolve the agent + derive its hot wallet from our master ------------
58
+ const resolved = await getAgentWithSigner(agentId);
59
+ if (!resolved) {
60
+ return res.status(404).json({ error: `agent #${agentId} is not registered on-chain` });
47
61
  }
62
+ const { agent, hotWallet, ownedByUs } = resolved;
48
63
  if (!agent.active) {
49
- return res.status(409).json({ error: `agent #${config.agentId} is inactive` });
64
+ return res.status(409).json({ error: `agent #${agentId} is inactive` });
65
+ }
66
+ if (!hotWallet) {
67
+ return res.status(409).json({ error: `agent #${agentId} has no hotWalletIndex in its metadata` });
68
+ }
69
+ if (!ownedByUs) {
70
+ return res.status(403).json({ error: `agent #${agentId} was not registered from this backend's master mnemonic` });
50
71
  }
51
72
 
73
+ // -- On-chain task preconditions ----------------------------------------
52
74
  const task = await getTask(taskId);
53
75
  if (!task) {
54
76
  return res.status(404).json({ error: "task not found on-chain" });
@@ -57,12 +79,14 @@ app.post("/solve", async (req, res) => {
57
79
  return res.status(409).json({ error: `task is not open (state=${task.state})` });
58
80
  }
59
81
 
60
- const decision = authorizeSolve({ req, caller, agent, task, taskId });
82
+ // -- Access policy: operator = free, creator = x402, others = 403 --------
83
+ const decision = authorizeSolve({ req, caller, agent, task, taskId, agentId });
61
84
  if (!decision.allow) {
62
85
  return res.status(decision.status).json(decision.body);
63
86
  }
64
87
 
65
- const job = startJob({ taskId, task, caller, path: decision.path });
88
+ // -- Accepted: queue the async pipeline for this agent -------------------
89
+ const job = startJob({ taskId, agentId, task, caller, path: decision.path, hotWallet });
66
90
  return res.status(202).json({ jobId: job.id, status: "queued" });
67
91
  } catch (err) {
68
92
  console.error(`[solve] ${err.stack || err.message}`);
@@ -78,8 +102,7 @@ app.get("/jobs/:id", (req, res) => {
78
102
 
79
103
  app.listen(config.port, () => {
80
104
  console.log(`__PROJECT_NAME__ listening on :${config.port}`);
81
- console.log(` agent id: #${config.agentId}`);
82
- console.log(` hot wallet: ${hotWallet.address}`);
83
- console.log(` solver: ${config.solver}${config.dryRun ? " (DRY_RUN)" : ""}`);
84
- console.log(` oracle: ${config.oracleUrl}`);
105
+ console.log(` master: ${masterFingerprint} (derivation index 0)`);
106
+ console.log(` solver: ${config.solver}${config.dryRun ? " (DRY_RUN)" : ""}`);
107
+ console.log(` oracle: ${config.oracleUrl}`);
85
108
  });
@@ -18,7 +18,7 @@ import path from "node:path";
18
18
  import { ethers } from "ethers";
19
19
  import { buildMarker, claim, OUTCOME_NAME } from "@jumboo/agent-sdk";
20
20
  import { config } from "./config.js";
21
- import { provider, hotWallet } from "./chain.js";
21
+ import { provider } from "./chain.js";
22
22
  import { getSolver } from "./solver.js";
23
23
  import {
24
24
  parseRepoUrl,
@@ -71,6 +71,7 @@ export function jobView(job) {
71
71
  return {
72
72
  jobId: job.id,
73
73
  taskId: job.taskId,
74
+ agentId: job.agentId ?? null,
74
75
  path: job.path,
75
76
  caller: job.caller,
76
77
  solver: job.solver,
@@ -88,10 +89,12 @@ export function jobView(job) {
88
89
  }
89
90
 
90
91
  /** Registers a queued job and kicks off the async pipeline. */
91
- export function startJob({ taskId, task, caller, path: accessPath }) {
92
+ export function startJob({ taskId, agentId, task, caller, path: accessPath, hotWallet }) {
92
93
  const job = {
93
94
  id: randomBytes(8).toString("hex"),
94
95
  taskId,
96
+ agentId,
97
+ hotWallet, // the agent's derived hot wallet (never serialized in jobView)
95
98
  task,
96
99
  caller,
97
100
  path: accessPath, // "compete" | "hire"
@@ -113,7 +116,7 @@ export function startJob({ taskId, task, caller, path: accessPath }) {
113
116
  }
114
117
 
115
118
  async function runJob(job) {
116
- const { task, taskId } = job;
119
+ const { task, taskId, agentId, hotWallet } = job;
117
120
  const parsed = parseRepoUrl(task.repoUrl);
118
121
  const repoDir = path.join(config.workdir, job.id);
119
122
  const branch = `jumboo/task-${taskId.slice(2, 10)}`;
@@ -148,7 +151,7 @@ async function runJob(job) {
148
151
  [
149
152
  "-c", `user.name=${user}`,
150
153
  "-c", `user.email=${user}@users.noreply.github.com`,
151
- "commit", "-m", `Jumboo task ${taskId.slice(0, 10)}… (agent #${config.agentId})`,
154
+ "commit", "-m", `Jumboo task ${taskId.slice(0, 10)}… (agent #${agentId})`,
152
155
  ],
153
156
  { cwd: repoDir }
154
157
  );
@@ -168,7 +171,7 @@ async function runJob(job) {
168
171
  }
169
172
 
170
173
  // 5. Build the marker block (SDK signs the RAW taskId bytes for us) ------
171
- const { marker } = await buildMarker({ taskId, agentId: config.agentId, hotWallet });
174
+ const { marker } = await buildMarker({ taskId, agentId, hotWallet });
172
175
  job.markerBlock = marker;
173
176
 
174
177
  // 6. Open the PR (or log the markers in DRY_RUN) -------------------------
@@ -187,7 +190,7 @@ async function runJob(job) {
187
190
  }
188
191
  const base = await getDefaultBranch(parsed.owner, parsed.repo);
189
192
  const pr = await createPullRequest(parsed.owner, parsed.repo, {
190
- title: `Jumboo task ${taskId.slice(0, 10)}… (agent #${config.agentId})`,
193
+ title: `Jumboo task ${taskId.slice(0, 10)}… (agent #${agentId})`,
191
194
  head: prHead,
192
195
  base,
193
196
  body: `${(job.summary || "").trim()}\n\n---\n${marker}`,
@@ -198,12 +201,16 @@ async function runJob(job) {
198
201
  // 7. Wait for the oracle's attestation, then claim on-chain (SDK) --------
199
202
  step = "awaiting-merge";
200
203
  setStatus(job, "awaiting-merge", `polling ${config.oracleUrl} every ${config.attestationPollSec}s`);
201
- const txWallet = new ethers.Wallet(config.agentTxKey, provider); // pays claim gas
204
+ // Gas payer: a shared funded wallet if AGENT_TX_KEY is set, else this agent's
205
+ // own derived hot wallet (which then needs a little ETH).
206
+ const txWallet = config.agentTxKey
207
+ ? new ethers.Wallet(config.agentTxKey, provider)
208
+ : hotWallet.connect(provider);
202
209
  const { attestation, receipt } = await claim({
203
210
  oracleUrl: config.oracleUrl,
204
211
  registryAddress: config.validationRegistryAddress,
205
212
  taskId,
206
- agentId: config.agentId,
213
+ agentId,
207
214
  wallet: txWallet,
208
215
  intervalMs: config.attestationPollSec * 1000,
209
216
  onPoll: ({ error }) => {
@@ -1,90 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Register this agent on-chain: mint the JumbooIdentityRegistry NFT to the
4
- * OPERATOR wallet, with the hot wallet as the registered signer.
5
- *
6
- * npm run register
7
- *
8
- * Reads .env directly (it must run BEFORE AGENT_ID exists, so it does not use
9
- * src/config.js, which requires AGENT_ID). On success it prints the new agentId
10
- * and reminds you to write it into .env.
11
- */
12
- import path from "node:path";
13
- import { fileURLToPath } from "node:url";
14
- import { config as loadEnv } from "dotenv";
15
- import { ethers } from "ethers";
16
-
17
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
- loadEnv({ path: path.join(__dirname, "..", ".env") });
19
-
20
- function required(name) {
21
- const v = process.env[name];
22
- if (!v) throw new Error(`Missing ${name} in .env`);
23
- return v;
24
- }
25
-
26
- const rpcUrl = required("RPC_URL");
27
- const identityRegistry = required("IDENTITY_REGISTRY_ADDRESS");
28
- const operatorKey = required("OPERATOR_KEY"); // owns the NFT, pays gas
29
- const hotKey = required("AGENT_HOT_KEY"); // the registered signer
30
-
31
- const provider = new ethers.JsonRpcProvider(rpcUrl);
32
- const operator = new ethers.Wallet(operatorKey, provider);
33
- const agentWallet = new ethers.Wallet(hotKey).address;
34
-
35
- // Build ERC-8004 registration metadata as a base64 data URI.
36
- function buildAgentUri() {
37
- let skills = [];
38
- try {
39
- skills = JSON.parse(process.env.AGENT_SKILLS || "[]");
40
- } catch {
41
- skills = String(process.env.AGENT_SKILLS || "").split(",").map((s) => s.trim()).filter(Boolean);
42
- }
43
- const backendUrl = (process.env.BACKEND_URL || "").replace(/\/+$/, "");
44
- const metadata = {
45
- type: "AgentCard",
46
- name: process.env.AGENT_NAME || "jumboo-agent",
47
- active: true,
48
- agentWallet,
49
- backendUrl,
50
- endpoints: { solve: backendUrl ? `${backendUrl}/solve` : "" },
51
- skills,
52
- pricing: {
53
- solve: {
54
- amount: process.env.HIRE_PRICE_AMOUNT || "0.50",
55
- asset: process.env.HIRE_PRICE_ASSET || "USDC",
56
- network: process.env.HIRE_PRICE_NETWORK || "sepolia",
57
- },
58
- },
59
- };
60
- const json = JSON.stringify(metadata);
61
- return `data:application/json;base64,${Buffer.from(json, "utf8").toString("base64")}`;
62
- }
63
-
64
- const ABI = [
65
- "function registerAgent(address agentWallet, string agentURI) external returns (uint256 agentId)",
66
- "event AgentRegistered(uint256 indexed agentId, address indexed operatorWallet, address indexed agentWallet, string agentURI)",
67
- ];
68
-
69
- const registry = new ethers.Contract(identityRegistry, ABI, operator);
70
-
71
- console.log(`Registering agent…`);
72
- console.log(` operator (NFT owner): ${operator.address}`);
73
- console.log(` agent hot wallet: ${agentWallet}`);
74
-
75
- const tx = await registry.registerAgent(agentWallet, buildAgentUri());
76
- console.log(` tx: ${tx.hash} — waiting for confirmation…`);
77
- const receipt = await tx.wait();
78
-
79
- let agentId = null;
80
- for (const log of receipt.logs) {
81
- try {
82
- const parsed = registry.interface.parseLog(log);
83
- if (parsed?.name === "AgentRegistered") agentId = parsed.args.agentId;
84
- } catch {}
85
- }
86
- if (agentId === null) throw new Error("AgentRegistered event not found — check the registry address/network");
87
-
88
- console.log(`\n✓ Registered. agentId = ${agentId}`);
89
- console.log(`\n Now set this in your .env:`);
90
- console.log(` AGENT_ID=${agentId}\n`);