@jumboo/create-agent 0.1.0 → 0.1.1
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 +10 -8
- package/bin/index.js +12 -24
- package/package.json +1 -1
- package/templates/default/README.md +13 -9
- package/templates/default/_env.example +7 -13
- package/templates/default/_package.json +2 -3
- package/templates/default/src/auth.js +4 -4
- package/templates/default/src/chain.js +44 -6
- package/templates/default/src/config.js +9 -11
- package/templates/default/src/index.js +37 -17
- package/templates/default/src/job.js +15 -8
- package/templates/default/scripts/register.js +0 -90
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
|
-
|
|
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
|
-
#
|
|
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 --
|
|
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
|
|
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
|
-
"#
|
|
121
|
-
"
|
|
122
|
-
|
|
123
|
-
"#
|
|
124
|
-
|
|
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
|
|
174
|
-
|
|
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
|
-
-
|
|
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
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
27
|
-
|
|
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` |
|
|
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
|
-
#
|
|
8
|
-
|
|
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
|
-
#
|
|
11
|
-
|
|
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 `
|
|
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.
|
|
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 #${
|
|
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
|
|
3
|
-
*
|
|
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
|
-
/**
|
|
11
|
-
export const
|
|
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
|
|
39
|
-
export async function getAgent(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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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)
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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 {
|
|
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
|
|
|
@@ -17,8 +21,8 @@ app.use(express.json());
|
|
|
17
21
|
app.get("/health", (_req, res) => {
|
|
18
22
|
res.json({
|
|
19
23
|
ok: true,
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
mode: "master",
|
|
25
|
+
master: masterFingerprint, // address of derivation index 0 (identifies the master)
|
|
22
26
|
solver: config.solver,
|
|
23
27
|
dryRun: config.dryRun,
|
|
24
28
|
jobs: jobCount(),
|
|
@@ -27,11 +31,17 @@ app.get("/health", (_req, res) => {
|
|
|
27
31
|
|
|
28
32
|
app.post("/solve", async (req, res) => {
|
|
29
33
|
try {
|
|
34
|
+
// -- Request shape ------------------------------------------------------
|
|
30
35
|
const taskId = req.body?.taskId;
|
|
31
36
|
if (!/^0x[0-9a-fA-F]{64}$/.test(taskId || "")) {
|
|
32
|
-
return res.status(400).json({ error: "body must
|
|
37
|
+
return res.status(400).json({ error: "body must include taskId: '0x<64 hex>'" });
|
|
38
|
+
}
|
|
39
|
+
const agentId = req.body?.agentId;
|
|
40
|
+
if (!/^\d+$/.test(String(agentId ?? ""))) {
|
|
41
|
+
return res.status(400).json({ error: "body must include agentId (the agent to compete as)" });
|
|
33
42
|
}
|
|
34
43
|
|
|
44
|
+
// -- Caller authentication (EIP-191 over `jumboo-solve:<taskId>`) --------
|
|
35
45
|
const caller = verifyCaller(req, taskId);
|
|
36
46
|
if (!caller) {
|
|
37
47
|
return res.status(401).json({
|
|
@@ -41,14 +51,23 @@ app.post("/solve", async (req, res) => {
|
|
|
41
51
|
});
|
|
42
52
|
}
|
|
43
53
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
// -- Resolve the agent + derive its hot wallet from our master ------------
|
|
55
|
+
const resolved = await getAgentWithSigner(agentId);
|
|
56
|
+
if (!resolved) {
|
|
57
|
+
return res.status(404).json({ error: `agent #${agentId} is not registered on-chain` });
|
|
47
58
|
}
|
|
59
|
+
const { agent, hotWallet, ownedByUs } = resolved;
|
|
48
60
|
if (!agent.active) {
|
|
49
|
-
return res.status(409).json({ error: `agent #${
|
|
61
|
+
return res.status(409).json({ error: `agent #${agentId} is inactive` });
|
|
62
|
+
}
|
|
63
|
+
if (!hotWallet) {
|
|
64
|
+
return res.status(409).json({ error: `agent #${agentId} has no hotWalletIndex in its metadata` });
|
|
65
|
+
}
|
|
66
|
+
if (!ownedByUs) {
|
|
67
|
+
return res.status(403).json({ error: `agent #${agentId} was not registered from this backend's master mnemonic` });
|
|
50
68
|
}
|
|
51
69
|
|
|
70
|
+
// -- On-chain task preconditions ----------------------------------------
|
|
52
71
|
const task = await getTask(taskId);
|
|
53
72
|
if (!task) {
|
|
54
73
|
return res.status(404).json({ error: "task not found on-chain" });
|
|
@@ -57,12 +76,14 @@ app.post("/solve", async (req, res) => {
|
|
|
57
76
|
return res.status(409).json({ error: `task is not open (state=${task.state})` });
|
|
58
77
|
}
|
|
59
78
|
|
|
60
|
-
|
|
79
|
+
// -- Access policy: operator = free, creator = x402, others = 403 --------
|
|
80
|
+
const decision = authorizeSolve({ req, caller, agent, task, taskId, agentId });
|
|
61
81
|
if (!decision.allow) {
|
|
62
82
|
return res.status(decision.status).json(decision.body);
|
|
63
83
|
}
|
|
64
84
|
|
|
65
|
-
|
|
85
|
+
// -- Accepted: queue the async pipeline for this agent -------------------
|
|
86
|
+
const job = startJob({ taskId, agentId, task, caller, path: decision.path, hotWallet });
|
|
66
87
|
return res.status(202).json({ jobId: job.id, status: "queued" });
|
|
67
88
|
} catch (err) {
|
|
68
89
|
console.error(`[solve] ${err.stack || err.message}`);
|
|
@@ -78,8 +99,7 @@ app.get("/jobs/:id", (req, res) => {
|
|
|
78
99
|
|
|
79
100
|
app.listen(config.port, () => {
|
|
80
101
|
console.log(`__PROJECT_NAME__ listening on :${config.port}`);
|
|
81
|
-
console.log(`
|
|
82
|
-
console.log(`
|
|
83
|
-
console.log(`
|
|
84
|
-
console.log(` oracle: ${config.oracleUrl}`);
|
|
102
|
+
console.log(` master: ${masterFingerprint} (derivation index 0)`);
|
|
103
|
+
console.log(` solver: ${config.solver}${config.dryRun ? " (DRY_RUN)" : ""}`);
|
|
104
|
+
console.log(` oracle: ${config.oracleUrl}`);
|
|
85
105
|
});
|
|
@@ -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
|
|
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 #${
|
|
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
|
|
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 #${
|
|
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
|
-
|
|
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
|
|
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`);
|