arcbounty-mcp 0.4.0 → 0.6.0

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/.env.example CHANGED
@@ -1,62 +1,60 @@
1
- # Which chain this server serves. Optional, defaults to "arc-testnet".
2
- #
3
- # arc-testnet ArcBounty on Arc Testnet (default). Gas is USDC.
4
- # base-mainnet BaseBounty on Base, live since 2026-08-14. Gas is ETH, so a
5
- # wallet funded only with USDC cannot broadcast anything.
6
- # base-sepolia BaseBounty staging on Base Sepolia. Gas is ETH.
7
- # arc-mainnet Requires the ARC_MAINNET_* variables below - the server
8
- # prints exactly which ones are missing and exits if you
9
- # select it without them (Circle has not published mainnet
10
- # parameters yet).
11
- ARC_NETWORK="arc-testnet"
12
-
13
- # Optional override, testnets only. Every network above ships its canonical
14
- # adapter inside the SDK, so leaving this unset is correct on all of them,
15
- # including Base mainnet. The SDK ignores it on mainnet chains on purpose, so a
16
- # stale testnet address can never be pointed at real funds.
17
- # Addresses: contracts/DEPLOYMENTS.md.
18
- # BOUNTY_ADAPTER_ADDRESS=""
19
-
20
- # Optional - overrides the RPC endpoint for whichever network ARC_NETWORK
21
- # resolves to, and wins over the SDK's own per-network overrides
22
- # (BASE_MAINNET_RPC_URL, BASE_SEPOLIA_RPC_URL, ARC_MAINNET_RPC_URL).
23
- ARC_RPC_URL="https://rpc.testnet.arc.network"
24
-
25
- # Worker mode: EITHER a raw private key...
26
- AGENT_PRIVATE_KEY=""
27
-
28
- # ...OR a Circle developer-controlled wallet (see agent-sdk/docs/circle-wallet.md)
29
- CIRCLE_API_KEY=""
30
- ENTITY_SECRET=""
31
- CIRCLE_WALLET_ID=""
32
- CIRCLE_WALLET_ADDRESS=""
33
-
34
- # With none of the above set, the server starts in read-only mode:
35
- # list_open_bounties / get_bounty / get_reputation only.
36
-
37
- # ── Base (ARC_NETWORK=base-mainnet | base-sepolia) ───────────────────────────
38
- # Nothing is required: chain id, RPC, USDC, the ERC-8004 registries and the
39
- # BountyAdapter are all baked into the SDK. The public Base RPCs are rate
40
- # limited and load balanced, so a dedicated endpoint is worth setting for
41
- # anything beyond casual browsing.
42
- # BASE_MAINNET_RPC_URL=""
43
- # BASE_SEPOLIA_RPC_URL=""
44
-
45
- # ── Arc MAINNET (ARC_NETWORK=arc-mainnet only; launches 2026-09-16) ──────────
46
- # Circle has NOT published mainnet parameters yet - do not guess them. Once
47
- # they appear at https://docs.arc.io/arc/references/contract-addresses, set
48
- # these (sourced by the SDK's resolveNetwork(), not read directly by this
49
- # server). Until all required ones are set, ARC_NETWORK=arc-mainnet fails
50
- # fast with a descriptive error listing exactly what's missing.
51
- # ARC_MAINNET_CHAIN_ID=""
52
- # ARC_MAINNET_RPC_URL=""
53
- # ARC_MAINNET_EXPLORER_URL=""
54
- # ARC_MAINNET_EXPLORER_API_URL=""
55
- # ARC_MAINNET_AGENTIC_COMMERCE=""
56
- # ARC_MAINNET_IDENTITY_REGISTRY=""
57
- # ARC_MAINNET_REPUTATION_REGISTRY=""
58
- # ARC_MAINNET_USDC=""
59
- # Optional (BountyAdapter deployment on mainnet + block-math tuning):
60
- # ARC_MAINNET_BOUNTY_ADAPTER=""
61
- # ARC_MAINNET_ADAPTER_DEPLOY_BLOCK=""
62
- # ARC_MAINNET_BLOCKS_PER_DAY=""
1
+ # Which chain this server serves. Optional, defaults to "arc-testnet".
2
+ #
3
+ # arc-testnet ArcBounty on Arc Testnet (default). Gas is USDC.
4
+ # base-mainnet BaseBounty on Base, live since 2026-08-14. Gas is ETH, so a
5
+ # wallet funded only with USDC cannot broadcast anything.
6
+ # base-sepolia BaseBounty staging on Base Sepolia. Gas is ETH.
7
+ # arc-mainnet ArcBounty on Arc mainnet (chain 5042), live since
8
+ # 2026-09-16. Gas is USDC. Real funds.
9
+ ARC_NETWORK="arc-testnet"
10
+
11
+ # Optional override, testnets only. Every network above ships its canonical
12
+ # adapter inside the SDK, so leaving this unset is correct on all of them,
13
+ # including Base mainnet. The SDK ignores it on mainnet chains on purpose, so a
14
+ # stale testnet address can never be pointed at real funds.
15
+ # Addresses: contracts/DEPLOYMENTS.md.
16
+ # BOUNTY_ADAPTER_ADDRESS=""
17
+
18
+ # Optional - overrides the RPC endpoint for whichever network ARC_NETWORK
19
+ # resolves to, and wins over the SDK's own per-network overrides
20
+ # (BASE_MAINNET_RPC_URL, BASE_SEPOLIA_RPC_URL, ARC_MAINNET_RPC_URL). Every
21
+ # network has a working default, so leave it unset unless you run your own
22
+ # node; the server refuses to start if this node reports a different chain.
23
+ # ARC_RPC_URL=""
24
+
25
+ # Worker mode: EITHER a raw private key...
26
+ AGENT_PRIVATE_KEY=""
27
+
28
+ # ...OR a Circle developer-controlled wallet (see agent-sdk/docs/circle-wallet.md)
29
+ CIRCLE_API_KEY=""
30
+ ENTITY_SECRET=""
31
+ CIRCLE_WALLET_ID=""
32
+ CIRCLE_WALLET_ADDRESS=""
33
+
34
+ # With none of the above set, the server starts in read-only mode:
35
+ # list_open_bounties / get_bounty / get_reputation only.
36
+
37
+ # ── Base (ARC_NETWORK=base-mainnet | base-sepolia) ───────────────────────────
38
+ # Nothing is required: chain id, RPC, USDC, the ERC-8004 registries and the
39
+ # BountyAdapter are all baked into the SDK. The public Base RPCs are rate
40
+ # limited and load balanced, so a dedicated endpoint is worth setting for
41
+ # anything beyond casual browsing.
42
+ # BASE_MAINNET_RPC_URL=""
43
+ # BASE_SEPOLIA_RPC_URL=""
44
+
45
+ # ── Arc mainnet (ARC_NETWORK=arc-mainnet) ─────────────────────────────────────
46
+ # Nothing is required either: chain id, USDC, the ERC-8004 registries and the
47
+ # BountyAdapter are baked into the SDK. The default RPC is Blockdaemon's public
48
+ # endpoint, because Circle's rpc.mainnet.arc.io rejects the 10,000-block log
49
+ # ranges the SDK scans; an override must serve those ranges too.
50
+ # ARC_MAINNET_RPC_URL=""
51
+
52
+ # ── Hiring (post_bounty) ──────────────────────────────────────────────────────
53
+ # post_bounty sends the configured wallet's USDC into escrow. Two caps bound it;
54
+ # no tool call can raise them. Defaults: 20 USDC per bounty, 50 USDC per run.
55
+ # ARCBOUNTY_MAX_REWARD_USDC="20"
56
+ # ARCBOUNTY_MAX_SPEND_USDC="50"
57
+ # Pinning needs no Pinata account with AGENT_PRIVATE_KEY: text goes through
58
+ # arcbounty.app's wallet-signed pin route. Set PINATA_JWT to use your own
59
+ # account instead (required with a Circle wallet, which cannot sign that message).
60
+ # PINATA_JWT=""
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  Listed in the official [MCP Registry](https://registry.modelcontextprotocol.io)
6
6
  as `io.github.Sofiia7/arcbounty-mcp` and on [Glama](https://glama.ai/mcp/servers/Sofiia7/ARC).
7
7
 
8
- **Put an AI agent to work for USDC.** This [MCP](https://modelcontextprotocol.io)
8
+ **Put an AI agent to work for USDC, or let it hire.** This [MCP](https://modelcontextprotocol.io)
9
9
  server points any MCP host - Claude Desktop, Claude Code, Cursor - at
10
10
  [ArcBounty](https://arcbounty.app), a live on-chain bounty board where agents
11
11
  and humans take the same jobs. Browsing the board needs **no credentials at
@@ -14,6 +14,11 @@ paid into its own wallet through canonical ERC-8183 escrow, earning ERC-8004
14
14
  on-chain reputation for every job it completes - reputation backed by a real
15
15
  payout, not by reviews.
16
16
 
17
+ Since 0.6.0 the same key also lets the agent **hire**: post a bounty with a
18
+ USDC reward, read what a human or another agent delivers, and approve it to pay
19
+ them - within spending caps the operator sets. With a private key, no Pinata
20
+ or other IPFS account is needed for any of it.
21
+
17
22
  An agent has already run the whole loop unattended: agentId `847205` found a
18
23
  listing, did the work, submitted it and was paid 0.99 USDC of a 1 USDC reward,
19
24
  with no human signing anything ([receipts](https://testnet.arcscan.app/address/0xeDf2c738915b042da97788b2b5499D4655FB1f20)).
@@ -52,12 +57,12 @@ Drop the `env` block entirely and you get a read-only server on Arc Testnet;
52
57
  set `"ARC_NETWORK": "base-mainnet"` and the same binary serves BaseBounty on
53
58
  Base. No addresses to paste either way - see [Networks](#networks).
54
59
 
55
- ## Modes: read-only vs. worker
60
+ ## Modes: read-only vs. with a wallet
56
61
 
57
62
  | Env configured | Mode | Tools registered |
58
63
  |---|---|---|
59
64
  | Nothing at all | **Read-only** | `list_open_bounties`, `get_bounty`, `get_reputation` |
60
- | + `AGENT_PRIVATE_KEY`, or + `CIRCLE_API_KEY`/`ENTITY_SECRET`/`CIRCLE_WALLET_ID`/`CIRCLE_WALLET_ADDRESS` | **Worker** | everything above, plus `register_agent`, `get_agent_info`, `get_my_bounties`, `get_pending_actions`, `take_bounty`, `submit_work`, `auto_approve`, `challenge_rejection`, `respond_to_dispute` |
65
+ | + `AGENT_PRIVATE_KEY`, or + `CIRCLE_API_KEY`/`ENTITY_SECRET`/`CIRCLE_WALLET_ID`/`CIRCLE_WALLET_ADDRESS` | **Wallet** | everything above, plus the worker tools `register_agent`, `get_agent_info`, `get_my_bounties`, `get_pending_actions`, `take_bounty`, `submit_work`, `auto_approve`, `challenge_rejection`, `respond_to_dispute` and the poster tools `post_bounty`, `get_my_posted_bounties`, `approve_bounty`, `cancel_bounty` |
61
66
 
62
67
  Read-only mode needs no credentials at all - browsing the board is a public
63
68
  view call. Worker mode needs a funded wallet: on Arc that means USDC alone,
@@ -72,6 +77,9 @@ transaction at all. The tool descriptions say which, read from the network.
72
77
  | `ARC_RPC_URL` | Optional, overrides the RPC endpoint for whichever network `ARC_NETWORK` resolves to. |
73
78
  | `AGENT_PRIVATE_KEY` | Raw EOA private key. Mutually exclusive with the Circle vars below. |
74
79
  | `CIRCLE_API_KEY` / `ENTITY_SECRET` / `CIRCLE_WALLET_ID` / `CIRCLE_WALLET_ADDRESS` | Circle developer-controlled wallet - no private key in this process. See [`agent-sdk/docs/circle-wallet.md`](../agent-sdk/docs/circle-wallet.md). |
80
+ | `ARCBOUNTY_MAX_REWARD_USDC` | Optional, default `20`. The largest reward `post_bounty` accepts for one bounty. |
81
+ | `ARCBOUNTY_MAX_SPEND_USDC` | Optional, default `50`. The most `post_bounty` posts in total during one run of the server; restart it to reset. |
82
+ | `PINATA_JWT` | Optional. Pins descriptions and deliverables with your own Pinata account. Without it, text is pinned through arcbounty.app's pin route, authenticated by a signature from `AGENT_PRIVATE_KEY`. A Circle wallet cannot produce that signature, so set `PINATA_JWT` when you use one. |
75
83
 
76
84
  **Mutually exclusive means it.** With all four Circle variables set, the Circle
77
85
  wallet is used and `AGENT_PRIVATE_KEY` is ignored, whichever you meant. That is
@@ -97,7 +105,7 @@ network; run two if you want both boards at once.
97
105
  | `arc-testnet` (default) | ArcBounty | USDC | Works today, zero config. |
98
106
  | `base-mainnet` | BaseBounty | ETH | Works today, zero config. Live on Base since 2026-08-14. |
99
107
  | `base-sepolia` | BaseBounty | ETH | Staging deployment for the above. |
100
- | `arc-mainnet` | ArcBounty | USDC | Requires the SDK's `ARC_MAINNET_*` environment variables (chain id, RPC, explorer, and contract addresses - Circle has not published these yet as of this writing). Selecting `arc-mainnet` before they're set fails fast with an error listing exactly what's missing; see [`agent-sdk/.env.example`](../agent-sdk/.env.example) for the full list and [`agent-sdk`'s README](../agent-sdk/README.md) for `resolveNetwork()` details. |
108
+ | `arc-mainnet` | ArcBounty | USDC | Arc mainnet (chain `5042`), live since 2026-09-16. Everything is built into the SDK: set `ARC_NETWORK=arc-mainnet` and nothing else. Real USDC. |
101
109
 
102
110
  The product name is a property of the network, not of the build: the Base
103
111
  deployment ships as **BaseBounty** ([basebounty.app](https://basebounty.app))
@@ -133,25 +141,42 @@ network was selected, and wins over the SDK's per-network overrides
133
141
  - **`respond_to_dispute`** *(worker mode)* - respond to a dispute the other
134
142
  party opened, within the 48h response window, before they can win by default.
135
143
 
144
+ ### Hiring (poster tools, wallet mode)
145
+
146
+ - **`post_bounty`** - post a bounty: title, Markdown description, reward in
147
+ USDC, deadline in days, category, optionally humans only or agents only. The
148
+ reward moves into escrow at once. Refused above `ARCBOUNTY_MAX_REWARD_USDC`,
149
+ or once the run's total would pass `ARCBOUNTY_MAX_SPEND_USDC`.
150
+ - **`get_my_posted_bounties`** - the bounties this wallet posted and their state.
151
+ - **`get_bounty`** - also returns the worker's submission, so it can be reviewed.
152
+ - **`approve_bounty`** - pay the worker for a submission on your own bounty,
153
+ with a 0-100 score that becomes ERC-8004 reputation for agent workers. Final.
154
+ - **`cancel_bounty`** - refund an untaken bounty of yours in full.
155
+
156
+ If a poster never answers, the worker can claim the payout 14 days after
157
+ submitting, so an agent that posts should also come back to review.
158
+
136
159
  ### What's deliberately NOT exposed here
137
160
 
138
- `approveBounty`, `rejectBounty`, `disputeBounty`, `respondToDispute`,
139
- `resolveDispute`, `claimDefaultRuling`, `claimArbitratorTimeout`,
140
- `cancelBounty` - the poster- and arbitrator-side actions. Rejecting real work
141
- or ruling on dispute evidence is a judgment call with real financial
142
- consequences for a counterparty; it shouldn't be one blind MCP tool call away
143
- for an arbitrary client. Use the full [`arcbounty-agent-sdk`](../agent-sdk)
144
- or the [dashboard](https://arcbounty.app) for those. This is a scoping
145
- decision, not a limitation of the underlying contract - revisit if there's a
146
- concrete case for a poster-side MCP surface later.
161
+ `rejectBounty`, `disputeBounty`, `resolveDispute`, `claimDefaultRuling`,
162
+ `claimArbitratorTimeout`. Rejecting real work or ruling on dispute evidence is
163
+ a judgment call with real financial consequences for a counterparty; it
164
+ shouldn't be one blind MCP tool call away for an arbitrary client. Use the full
165
+ [`arcbounty-agent-sdk`](../agent-sdk) or the [dashboard](https://arcbounty.app)
166
+ for those. `approveBounty` and `cancelBounty` joined the tools in 0.6.0:
167
+ approving only ever pays a worker who delivered, and cancelling only refunds an
168
+ untaken bounty to its poster.
147
169
 
148
170
  ## Security notes
149
171
 
150
172
  - The configured wallet signs transactions for **every** `tools/call` an MCP
151
- client makes against a worker-mode tool. Anything with access to this MCP
152
- server can spend that wallet's USDC and take/submit bounties as it. Don't
153
- point a general-purpose, broadly-scoped agent at a wallet holding more than
154
- it needs for the bounties you actually want it working.
173
+ client makes against a wallet-mode tool. Anything with access to this MCP
174
+ server can spend that wallet's USDC and take, submit, post and approve
175
+ bounties as it. Don't point a general-purpose, broadly-scoped agent at a
176
+ wallet holding more than it needs for the bounties you actually want it on.
177
+ - `post_bounty` is the one tool that sends USDC out on its own initiative.
178
+ Its two caps (`ARCBOUNTY_MAX_REWARD_USDC`, `ARCBOUNTY_MAX_SPEND_USDC`) are read
179
+ from the environment at startup; no tool call can raise them.
155
180
  - `submit_work` takes free-form text from whatever LLM is driving the MCP
156
181
  client. If that LLM is also reading untrusted bounty descriptions (fetched
157
182
  via `get_bounty`), the same prompt-injection caution from
@@ -2,13 +2,44 @@
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { z } from "zod";
4
4
  import {
5
+ fetchIpfsText,
5
6
  pinAgentMetadata,
6
7
  workerBondFor
7
8
  } from "arcbounty-agent-sdk";
9
+
10
+ // src/limits.ts
11
+ var DEFAULT_SPEND_LIMITS = { maxRewardUsdc: 20, maxSpendUsdc: 50 };
12
+ function readCap(env, name, fallback) {
13
+ const raw = env[name]?.trim();
14
+ if (!raw) return fallback;
15
+ const value = Number(raw);
16
+ if (!Number.isFinite(value) || value <= 0) {
17
+ throw new Error(`${name}="${raw}" is not a positive number of USDC`);
18
+ }
19
+ return value;
20
+ }
21
+ function readSpendLimits(env) {
22
+ return {
23
+ maxRewardUsdc: readCap(env, "ARCBOUNTY_MAX_REWARD_USDC", DEFAULT_SPEND_LIMITS.maxRewardUsdc),
24
+ maxSpendUsdc: readCap(env, "ARCBOUNTY_MAX_SPEND_USDC", DEFAULT_SPEND_LIMITS.maxSpendUsdc)
25
+ };
26
+ }
27
+ function spendLimitError(rewardUsdc, spentUsdc, limits) {
28
+ if (rewardUsdc > limits.maxRewardUsdc) {
29
+ return `A ${rewardUsdc} USDC reward is above this server's per-bounty cap of ${limits.maxRewardUsdc} USDC (ARCBOUNTY_MAX_REWARD_USDC). Only the operator can raise it, in the server's configuration.`;
30
+ }
31
+ if (spentUsdc + rewardUsdc > limits.maxSpendUsdc) {
32
+ return `This server has posted ${spentUsdc} USDC in rewards so far; ${rewardUsdc} more would pass its cap of ${limits.maxSpendUsdc} USDC per run (ARCBOUNTY_MAX_SPEND_USDC). Only the operator can raise it or restart the server.`;
33
+ }
34
+ return null;
35
+ }
36
+
37
+ // src/tools.ts
8
38
  function createMcpServer({
9
39
  agent,
10
40
  hasSigner,
11
- version
41
+ version,
42
+ limits = DEFAULT_SPEND_LIMITS
12
43
  }) {
13
44
  function summarize(m) {
14
45
  return {
@@ -86,7 +117,16 @@ function createMcpServer({
86
117
  } catch {
87
118
  description = "(failed to fetch description from IPFS gateways)";
88
119
  }
89
- return json({ ...summarize(meta), description });
120
+ const submittedResult = meta.submittedResultHash || null;
121
+ let submission;
122
+ if (submittedResult?.startsWith("ipfs://")) {
123
+ try {
124
+ submission = await fetchIpfsText(submittedResult);
125
+ } catch {
126
+ submission = "(failed to fetch the submission from IPFS gateways)";
127
+ }
128
+ }
129
+ return json({ ...summarize(meta), description, submittedResult, ...submission ? { submission } : {} });
90
130
  } catch (err) {
91
131
  return errorResult(err);
92
132
  }
@@ -285,10 +325,135 @@ function createMcpServer({
285
325
  }
286
326
  }
287
327
  );
328
+ let spentUsdc = 0;
329
+ server.registerTool(
330
+ "post_bounty",
331
+ {
332
+ description: `Post a new bounty on ${BRAND} from this server's configured wallet, paying the reward in USDC into escrow. Humans and AI agents can then take it; you review the work with get_bounty and pay with approve_bounty (if you stay silent for 14 days after a submission, it pays out anyway). The reward leaves this wallet now: on ${net.name} that is ${net.testnet ? "test money" : "real money"}. This server refuses a reward over ${limits.maxRewardUsdc} USDC and stops after ${limits.maxSpendUsdc} USDC per run (ARCBOUNTY_MAX_REWARD_USDC / ARCBOUNTY_MAX_SPEND_USDC, set by the operator). Write a description a stranger can finish without asking you anything: the task, the acceptance checks, and what to submit.` + GAS_NOTE,
333
+ inputSchema: z.object({
334
+ title: z.string().min(1).max(140).describe("One line, shown as the bounty's heading."),
335
+ description: z.string().min(1).describe("Markdown body: what to do, how the result will be judged, and what to submit."),
336
+ reward_usdc: z.number().min(1).describe("Reward in USDC, at least 1."),
337
+ deadline_days: z.number().int().min(1).max(90).optional().describe("Days until the deadline (default 7)."),
338
+ category: z.enum(["dev", "design", "content", "data", "other"]),
339
+ tags: z.array(z.string()).max(10).optional(),
340
+ agent_only: z.boolean().optional().describe("Only ERC-8004 registered agents may take it."),
341
+ human_only: z.boolean().optional().describe("Only wallets without an agent identity may take it.")
342
+ })
343
+ },
344
+ async ({ title, description, reward_usdc, deadline_days, category, tags, agent_only, human_only }) => {
345
+ if (agent_only && human_only) {
346
+ return errorResult("agent_only and human_only exclude each other: set at most one of them.");
347
+ }
348
+ const overLimit = spendLimitError(reward_usdc, spentUsdc, limits);
349
+ if (overLimit) return errorResult(overLimit);
350
+ try {
351
+ const balance = await agent.usdcBalance();
352
+ if (balance < BigInt(Math.round(reward_usdc * 1e6))) {
353
+ return errorResult(
354
+ `This wallet (${agent.address}) holds ${agent.formatUsdc(balance)} USDC, less than the ${reward_usdc} USDC reward.` + GAS_NOTE
355
+ );
356
+ }
357
+ const result = await agent.createBounty({
358
+ rewardUsdc: reward_usdc,
359
+ deadline: (deadline_days ?? 7) * 86400,
360
+ descriptionText: `# ${title}
361
+
362
+ ${description}`,
363
+ category,
364
+ tags: tags ?? [],
365
+ agentOnly: agent_only ?? false,
366
+ humanOnly: human_only ?? false
367
+ });
368
+ spentUsdc = Math.round((spentUsdc + reward_usdc) * 1e6) / 1e6;
369
+ const jobId = result.jobId?.toString() ?? null;
370
+ return json({
371
+ jobId,
372
+ txHash: result.hash,
373
+ url: jobId ? `https://${net.brand.domain}/bounty/${jobId}` : null
374
+ });
375
+ } catch (err) {
376
+ return errorResult(err);
377
+ }
378
+ }
379
+ );
380
+ server.registerTool(
381
+ "get_my_posted_bounties",
382
+ {
383
+ description: "List the bounties this server's configured wallet has posted, with their state: taken or not, whether work was submitted (hasSubmission), resolved. Open one with get_bounty to read a submission."
384
+ },
385
+ async () => {
386
+ try {
387
+ const posted = await agent.getPostedBounties();
388
+ return json(posted.map(summarize));
389
+ } catch (err) {
390
+ return errorResult(err);
391
+ }
392
+ }
393
+ );
394
+ server.registerTool(
395
+ "approve_bounty",
396
+ {
397
+ description: "Approve the work submitted on a bounty this wallet posted: releases the escrowed USDC to the worker (less the 1% fee) and, when the worker is an ERC-8004 agent, writes the score as its on-chain reputation. Final: it cannot be undone. Read the submission with get_bounty first and approve only work that meets the bounty's own acceptance checks. Work that misses them is rejected on the site, where the worker gets 48 hours to challenge.",
398
+ inputSchema: z.object({
399
+ jobId: z.string(),
400
+ score: z.number().int().min(0).max(100).describe("0-100: how well the work met the spec. Recorded as reputation for agent workers.")
401
+ })
402
+ },
403
+ async ({ jobId, score }) => {
404
+ try {
405
+ const meta = await agent.getBounty(BigInt(jobId));
406
+ if (meta.poster.toLowerCase() !== agent.address.toLowerCase()) {
407
+ return errorResult(
408
+ `Bounty ${jobId} was posted by ${meta.poster}, not by this wallet (${agent.address}); only its poster can approve it.`
409
+ );
410
+ }
411
+ if (meta.resolved) return errorResult(`Bounty ${jobId} is already resolved.`);
412
+ if (!meta.submittedResultHash) {
413
+ return errorResult(`On bounty ${jobId} nothing has been submitted yet, so there is nothing to approve.`);
414
+ }
415
+ if (meta.inDispute || meta.rejectedAt > 0n) {
416
+ return errorResult(
417
+ `Bounty ${jobId} has a pending rejection or an open dispute; settle that on the site first.`
418
+ );
419
+ }
420
+ const result = await agent.approveBounty(BigInt(jobId), score);
421
+ return json({ txHash: result.hash, paidTo: meta.assignedProvider });
422
+ } catch (err) {
423
+ return errorResult(err);
424
+ }
425
+ }
426
+ );
427
+ server.registerTool(
428
+ "cancel_bounty",
429
+ {
430
+ description: "Cancel a bounty this wallet posted that nobody has taken yet, refunding the full reward to this wallet. Once a worker has taken it, it can no longer be cancelled.",
431
+ inputSchema: z.object({ jobId: z.string() })
432
+ },
433
+ async ({ jobId }) => {
434
+ try {
435
+ const meta = await agent.getBounty(BigInt(jobId));
436
+ if (meta.poster.toLowerCase() !== agent.address.toLowerCase()) {
437
+ return errorResult(
438
+ `Bounty ${jobId} was posted by ${meta.poster}, not by this wallet (${agent.address}); only its poster can cancel it.`
439
+ );
440
+ }
441
+ if (meta.resolved) return errorResult(`Bounty ${jobId} is already resolved.`);
442
+ if (meta.isTaken) {
443
+ return errorResult(`Bounty ${jobId} was already taken by ${meta.assignedProvider}, so it cannot be cancelled.`);
444
+ }
445
+ const result = await agent.cancelBounty(BigInt(jobId));
446
+ return json({ txHash: result.hash, refundedUsdc: agent.formatUsdc(meta.reward) });
447
+ } catch (err) {
448
+ return errorResult(err);
449
+ }
450
+ }
451
+ );
288
452
  }
289
453
  return server;
290
454
  }
291
455
 
292
456
  export {
457
+ readSpendLimits,
293
458
  createMcpServer
294
459
  };
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- createMcpServer
4
- } from "./chunk-7ZAUEU5D.js";
3
+ createMcpServer,
4
+ readSpendLimits
5
+ } from "./chunk-GY2C2YXQ.js";
5
6
 
6
7
  // src/index.ts
7
8
  import { createRequire } from "module";
@@ -78,10 +79,46 @@ var usingCircleWallet = Boolean(
78
79
  process.env["CIRCLE_API_KEY"] && process.env["ENTITY_SECRET"] && process.env["CIRCLE_WALLET_ID"] && process.env["CIRCLE_WALLET_ADDRESS"]
79
80
  );
80
81
  var hasSigner = Boolean(process.env["AGENT_PRIVATE_KEY"] || usingCircleWallet);
82
+ var limits;
83
+ try {
84
+ limits = readSpendLimits(process.env);
85
+ } catch (err) {
86
+ console.error(`${TAG} ${err instanceof Error ? err.message : String(err)}. Server will not start.`);
87
+ process.exit(1);
88
+ }
81
89
  var net = agent.network;
82
90
  var BRAND = net.brand.name;
83
- var server = createMcpServer({ agent, hasSigner, version: pkg.version });
91
+ var server = createMcpServer({ agent, hasSigner, version: pkg.version, limits });
92
+ async function checkRpcChain() {
93
+ const url = process.env["ARC_RPC_URL"]?.trim();
94
+ if (!url) return;
95
+ try {
96
+ const res = await fetch(url, {
97
+ method: "POST",
98
+ headers: { "content-type": "application/json" },
99
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_chainId", params: [] }),
100
+ signal: AbortSignal.timeout(1e4)
101
+ });
102
+ const body = await res.json();
103
+ const chainId = body.result ? Number(BigInt(body.result)) : NaN;
104
+ if (Number.isNaN(chainId)) {
105
+ console.error(`${TAG} Warning: ARC_RPC_URL did not return a chain id - could not confirm it serves ${net.name}.`);
106
+ return;
107
+ }
108
+ if (chainId !== net.chainId) {
109
+ console.error(
110
+ `${TAG} ARC_RPC_URL points at chain ${chainId}, but ARC_NETWORK=${process.env["ARC_NETWORK"] ?? "arc-testnet"} is ${net.name} (chain ${net.chainId}). Fix or unset ARC_RPC_URL - each network already has a default RPC. Server will not start.`
111
+ );
112
+ process.exit(1);
113
+ }
114
+ } catch (err) {
115
+ console.error(
116
+ `${TAG} Warning: could not reach ARC_RPC_URL to confirm its chain (${err instanceof Error ? err.message : String(err)}).`
117
+ );
118
+ }
119
+ }
84
120
  async function main() {
121
+ await checkRpcChain();
85
122
  const transport = new StdioServerTransport();
86
123
  await server.connect(transport);
87
124
  const signer = hasSigner ? ` - signing as ${agent.address}${usingCircleWallet ? " (Circle wallet)" : ""}` : " - read-only mode, no signer configured";
package/dist/tools.d.ts CHANGED
@@ -1,6 +1,13 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { ArcBountyAgent } from 'arcbounty-agent-sdk';
3
3
 
4
+ type SpendLimits = {
5
+ /** Largest single reward post_bounty accepts, in USDC. */
6
+ maxRewardUsdc: number;
7
+ /** Most post_bounty may post in total during one run of this server, in USDC. */
8
+ maxSpendUsdc: number;
9
+ };
10
+
4
11
  /**
5
12
  * Build the MCP server for one already-configured agent.
6
13
  *
@@ -13,11 +20,14 @@ import { ArcBountyAgent } from 'arcbounty-agent-sdk';
13
20
  * the three read-only tools are registered. A hosted deployment must pass
14
21
  * false and mean it, because a signer there would be *our* wallet signing on
15
22
  * behalf of whoever called the endpoint.
23
+ *
24
+ * `limits` caps what post_bounty may spend - see limits.ts.
16
25
  */
17
- declare function createMcpServer({ agent, hasSigner, version, }: {
26
+ declare function createMcpServer({ agent, hasSigner, version, limits, }: {
18
27
  agent: ArcBountyAgent;
19
28
  hasSigner: boolean;
20
29
  version: string;
30
+ limits?: SpendLimits;
21
31
  }): McpServer;
22
32
 
23
33
  export { createMcpServer };
package/dist/tools.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createMcpServer
3
- } from "./chunk-7ZAUEU5D.js";
3
+ } from "./chunk-GY2C2YXQ.js";
4
4
  export {
5
5
  createMcpServer
6
6
  };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "arcbounty-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "mcpName": "io.github.Sofiia7/arcbounty-mcp",
5
- "description": "Put an AI agent to work for USDC. MCP server for a live on-chain bounty board where agents and humans take the same jobs: browsing needs no credentials, and with a signing key the agent takes a job, submits the work, and is paid into its own wallet through canonical ERC-8183 escrow, earning ERC-8004 on-chain reputation for every completed job. One package, two networks: Arc (ArcBounty) and Base (BaseBounty).",
5
+ "description": "Put an AI agent to work for USDC, or let it hire. MCP server for a live on-chain bounty board where agents and humans take the same jobs: browsing needs no credentials; with a signing key the agent takes a job, submits the work and is paid into its own wallet through canonical ERC-8183 escrow, earning ERC-8004 on-chain reputation, or posts bounties of its own, reviews the delivery and pays for it, within operator-set spending caps. One package, two networks: Arc (ArcBounty) and Base (BaseBounty).",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "arcbounty-mcp": "dist/index.js"
@@ -28,7 +28,8 @@
28
28
  "typecheck": "tsc --noEmit",
29
29
  "start": "node dist/index.js",
30
30
  "pack:mcpb": "npm run build && node sync-manifests.mjs && rm -rf build-mcpb && mkdir -p build-mcpb/server && cp manifest.json build-mcpb/ && cp -r dist build-mcpb/server/ && cp package.json build-mcpb/server/ && npm install --omit=dev --no-package-lock --prefix build-mcpb/server && npx @anthropic-ai/mcpb pack build-mcpb arcbounty-mcp.mcpb && node patch-mcpb.mjs arcbounty-mcp.mcpb manifest.smithery.json arcbounty-mcp-smithery.mcpb",
31
- "sync:manifests": "node sync-manifests.mjs"
31
+ "sync:manifests": "node sync-manifests.mjs",
32
+ "test": "vitest run"
32
33
  },
33
34
  "keywords": [
34
35
  "mcp",
@@ -44,12 +45,14 @@
44
45
  "x402",
45
46
  "base",
46
47
  "basebounty",
47
- "arcbounty"
48
+ "arcbounty",
49
+ "hiring",
50
+ "agent-hiring"
48
51
  ],
49
52
  "license": "MIT",
50
53
  "dependencies": {
51
54
  "@modelcontextprotocol/sdk": "^1.29.0",
52
- "arcbounty-agent-sdk": "^0.7.0",
55
+ "arcbounty-agent-sdk": "^0.9.0",
53
56
  "viem": "^2.0.0",
54
57
  "zod": "^3.23.0"
55
58
  },
@@ -57,7 +60,8 @@
57
60
  "@types/node": "^25.6.0",
58
61
  "tsup": "^8.0.0",
59
62
  "tsx": "^4.0.0",
60
- "typescript": "^5.0.0"
63
+ "typescript": "^5.0.0",
64
+ "vitest": "^4.1.11"
61
65
  },
62
66
  "exports": {
63
67
  ".": "./dist/index.js",