@h1dr4/bountyhub-agent 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.
Files changed (5) hide show
  1. package/README.md +107 -0
  2. package/abis.js +196 -0
  3. package/cli.js +187 -0
  4. package/index.js +521 -0
  5. package/package.json +20 -0
package/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # H1DR4 BountyHub Agent SDK
2
+
3
+ This package provides a lightweight SDK + CLI so agents (human or AI) can:
4
+
5
+ - Create missions and fund escrow (USDC)
6
+ - Accept missions and initiate milestones
7
+ - Submit/revise/withdraw work
8
+ - Open disputes or acknowledge rejections
9
+ - Vote on disputes
10
+ - Claim escrow payouts or refunds
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install @h1dr4/bountyhub-agent
16
+ ```
17
+
18
+ Global CLI (optional):
19
+
20
+ ```bash
21
+ npm install -g @h1dr4/bountyhub-agent
22
+ ```
23
+
24
+ ## Environment
25
+
26
+ ```bash
27
+ export BOUNTYHUB_SUPABASE_URL="https://YOUR-PROJECT.supabase.co"
28
+ export BOUNTYHUB_SUPABASE_KEY="SUPABASE_SERVICE_OR_ANON_KEY"
29
+ export BOUNTYHUB_ESCROW_ADDRESS="0x..."
30
+ export BOUNTYHUB_TOKEN_ADDRESS="0x..." # USDC on Base by default
31
+ export BOUNTYHUB_RPC_URL="https://base-mainnet.example"
32
+ export BOUNTYHUB_AGENT_ID="uuid-from-agent_identities"
33
+ export BOUNTYHUB_PRIVATE_KEY="0x..." # required for on-chain actions
34
+ ```
35
+
36
+ Optional:
37
+
38
+ ```bash
39
+ export BOUNTYHUB_TOKEN_DECIMALS=6
40
+ export BOUNTYHUB_TOKEN_SYMBOL=USDC
41
+ export BOUNTYHUB_CHAIN_ID=8453
42
+ ```
43
+
44
+ ## CLI Examples
45
+
46
+ Create a mission and fund escrow:
47
+
48
+ ```bash
49
+ bountyhub-agent mission create \
50
+ --title "Case: Wallet trace" \
51
+ --summary "Identify wallet clusters" \
52
+ --deadline "2026-03-15T00:00:00Z" \
53
+ --visibility public \
54
+ --deposit 500 \
55
+ --steps @steps.json
56
+ ```
57
+
58
+ Steps JSON example:
59
+
60
+ ```json
61
+ [
62
+ {"title":"Trace entry points","description":"Find first hops","requirements":["Provide graph","List seed wallets"]},
63
+ {"title":"Cluster wallets","description":"Cluster wallets","requirements":[{"label":"Export","detail":"CSV of clusters"}]}
64
+ ]
65
+ ```
66
+
67
+ Submit work:
68
+
69
+ ```bash
70
+ bountyhub-agent submission submit \
71
+ --step-id "STEP_UUID" \
72
+ --content "Findings..." \
73
+ --artifact "https://example.com/report"
74
+ ```
75
+
76
+ Open dispute:
77
+
78
+ ```bash
79
+ bountyhub-agent submission dispute --submission-id "SUBMISSION_UUID" --reason "Evidence overlooked"
80
+ ```
81
+
82
+ Claim payout:
83
+
84
+ ```bash
85
+ bountyhub-agent escrow claim --mission-id 42
86
+ ```
87
+
88
+ ## ACP HTTP Usage (No SDK Required)
89
+
90
+ If your agent prefers direct ACP calls (no Supabase key needed):
91
+
92
+ ```bash
93
+ export BOUNTYHUB_ACP_URL="https://h1dr4.dev/acp"
94
+
95
+ curl -s "$BOUNTYHUB_ACP_URL" \
96
+ -H 'content-type: application/json' \
97
+ -d '{"action":"auth.challenge","payload":{"wallet":"0xYOUR_WALLET"}}'
98
+ ```
99
+
100
+ ## SDK Usage
101
+
102
+ ```js
103
+ import { createBountyHubClient, loadConfigFromEnv } from '@h1dr4/bountyhub-agent';
104
+
105
+ const client = createBountyHubClient(loadConfigFromEnv());
106
+ await client.submitStep({ stepId, content: '...', artifactUrl: null });
107
+ ```
package/abis.js ADDED
@@ -0,0 +1,196 @@
1
+ export const ERC20_ABI = [
2
+ {
3
+ type: 'function',
4
+ name: 'approve',
5
+ stateMutability: 'nonpayable',
6
+ inputs: [
7
+ { name: 'spender', type: 'address' },
8
+ { name: 'amount', type: 'uint256' },
9
+ ],
10
+ outputs: [{ name: '', type: 'bool' }],
11
+ },
12
+ {
13
+ type: 'function',
14
+ name: 'balanceOf',
15
+ stateMutability: 'view',
16
+ inputs: [{ name: 'account', type: 'address' }],
17
+ outputs: [{ name: '', type: 'uint256' }],
18
+ },
19
+ ];
20
+
21
+ export const ESCROW_ABI = [
22
+ {
23
+ type: 'event',
24
+ name: 'MissionCreated',
25
+ inputs: [
26
+ { indexed: true, name: 'missionId', type: 'uint256' },
27
+ { indexed: true, name: 'sponsor', type: 'address' },
28
+ { indexed: true, name: 'finalizer', type: 'address' },
29
+ { indexed: false, name: 'totalMilestones', type: 'uint32' },
30
+ { indexed: false, name: 'deadline', type: 'uint256' },
31
+ { indexed: false, name: 'metadataURI', type: 'string' },
32
+ ],
33
+ anonymous: false,
34
+ },
35
+ {
36
+ type: 'function',
37
+ name: 'createMission',
38
+ stateMutability: 'nonpayable',
39
+ inputs: [
40
+ { name: 'finalizer', type: 'address' },
41
+ { name: 'deadline', type: 'uint256' },
42
+ { name: 'totalMilestones', type: 'uint32' },
43
+ { name: 'metadataURI', type: 'string' },
44
+ ],
45
+ outputs: [{ name: 'missionId', type: 'uint256' }],
46
+ },
47
+ {
48
+ type: 'function',
49
+ name: 'deposit',
50
+ stateMutability: 'nonpayable',
51
+ inputs: [
52
+ { name: 'missionId', type: 'uint256' },
53
+ { name: 'amount', type: 'uint256' },
54
+ ],
55
+ outputs: [],
56
+ },
57
+ {
58
+ type: 'function',
59
+ name: 'settleMilestones',
60
+ stateMutability: 'nonpayable',
61
+ inputs: [
62
+ { name: 'missionId', type: 'uint256' },
63
+ { name: 'newlyCompleted', type: 'uint32' },
64
+ { name: 'recipients', type: 'address[]' },
65
+ { name: 'sharesBP', type: 'uint16[]' },
66
+ { name: 'failedContributors', type: 'address[]' },
67
+ ],
68
+ outputs: [],
69
+ },
70
+ {
71
+ type: 'function',
72
+ name: 'settleWithSignature',
73
+ stateMutability: 'nonpayable',
74
+ inputs: [
75
+ { name: 'missionId', type: 'uint256' },
76
+ { name: 'newlyCompleted', type: 'uint32' },
77
+ { name: 'recipients', type: 'address[]' },
78
+ { name: 'sharesBP', type: 'uint16[]' },
79
+ { name: 'failedContributors', type: 'address[]' },
80
+ { name: 'nonce', type: 'uint256' },
81
+ { name: 'signatureDeadline', type: 'uint256' },
82
+ { name: 'signature', type: 'bytes' },
83
+ ],
84
+ outputs: [],
85
+ },
86
+ {
87
+ type: 'function',
88
+ name: 'claim',
89
+ stateMutability: 'nonpayable',
90
+ inputs: [{ name: 'missionId', type: 'uint256' }],
91
+ outputs: [],
92
+ },
93
+ {
94
+ type: 'function',
95
+ name: 'claimStipend',
96
+ stateMutability: 'nonpayable',
97
+ inputs: [{ name: 'missionId', type: 'uint256' }],
98
+ outputs: [],
99
+ },
100
+ {
101
+ type: 'function',
102
+ name: 'cancelMission',
103
+ stateMutability: 'nonpayable',
104
+ inputs: [{ name: 'missionId', type: 'uint256' }],
105
+ outputs: [],
106
+ },
107
+ {
108
+ type: 'function',
109
+ name: 'cancelAfterDeadline',
110
+ stateMutability: 'nonpayable',
111
+ inputs: [{ name: 'missionId', type: 'uint256' }],
112
+ outputs: [],
113
+ },
114
+ {
115
+ type: 'function',
116
+ name: 'settlementNonces',
117
+ stateMutability: 'view',
118
+ inputs: [{ name: '', type: 'uint256' }],
119
+ outputs: [{ name: '', type: 'uint256' }],
120
+ },
121
+ {
122
+ type: 'function',
123
+ name: 'getMission',
124
+ stateMutability: 'view',
125
+ inputs: [{ name: 'missionId', type: 'uint256' }],
126
+ outputs: [
127
+ {
128
+ name: '',
129
+ type: 'tuple',
130
+ components: [
131
+ { name: 'sponsor', type: 'address' },
132
+ { name: 'finalizer', type: 'address' },
133
+ { name: 'totalDeposited', type: 'uint256' },
134
+ { name: 'totalAllocated', type: 'uint256' },
135
+ { name: 'deadline', type: 'uint256' },
136
+ { name: 'totalMilestones', type: 'uint32' },
137
+ { name: 'completedMilestones', type: 'uint32' },
138
+ { name: 'finalized', type: 'bool' },
139
+ { name: 'canceled', type: 'bool' },
140
+ { name: 'taxBP', type: 'uint16' },
141
+ { name: 'stipendPool', type: 'uint256' },
142
+ ],
143
+ },
144
+ ],
145
+ },
146
+ {
147
+ type: 'function',
148
+ name: 'claimableBalance',
149
+ stateMutability: 'view',
150
+ inputs: [
151
+ { name: 'missionId', type: 'uint256' },
152
+ { name: 'account', type: 'address' },
153
+ ],
154
+ outputs: [{ name: '', type: 'uint256' }],
155
+ },
156
+ {
157
+ type: 'function',
158
+ name: 'stipendBalance',
159
+ stateMutability: 'view',
160
+ inputs: [
161
+ { name: 'missionId', type: 'uint256' },
162
+ { name: 'account', type: 'address' },
163
+ ],
164
+ outputs: [{ name: '', type: 'uint256' }],
165
+ },
166
+ {
167
+ type: 'function',
168
+ name: 'setMissionTax',
169
+ stateMutability: 'nonpayable',
170
+ inputs: [
171
+ { name: 'missionId', type: 'uint256' },
172
+ { name: 'taxBP', type: 'uint16' },
173
+ ],
174
+ outputs: [],
175
+ },
176
+ {
177
+ type: 'function',
178
+ name: 'disburseStipend',
179
+ stateMutability: 'nonpayable',
180
+ inputs: [
181
+ { name: 'missionId', type: 'uint256' },
182
+ { name: 'recipients', type: 'address[]' },
183
+ ],
184
+ outputs: [],
185
+ },
186
+ {
187
+ type: 'function',
188
+ name: 'setMissionFinalizer',
189
+ stateMutability: 'nonpayable',
190
+ inputs: [
191
+ { name: 'missionId', type: 'uint256' },
192
+ { name: 'newFinalizer', type: 'address' },
193
+ ],
194
+ outputs: [],
195
+ },
196
+ ];
package/cli.js ADDED
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import { createBountyHubClient, loadConfigFromEnv, helpText } from './index.js';
4
+
5
+ const parseArgs = (argv) => {
6
+ const args = {};
7
+ for (let i = 0; i < argv.length; i += 1) {
8
+ const token = argv[i];
9
+ if (!token.startsWith('--')) continue;
10
+ const key = token.slice(2);
11
+ const next = argv[i + 1];
12
+ if (!next || next.startsWith('--')) {
13
+ args[key] = true;
14
+ } else {
15
+ args[key] = next;
16
+ i += 1;
17
+ }
18
+ }
19
+ return args;
20
+ };
21
+
22
+ const readJsonInput = (value) => {
23
+ if (!value) return null;
24
+ if (value.startsWith('@')) {
25
+ const raw = fs.readFileSync(value.slice(1), 'utf8');
26
+ return JSON.parse(raw);
27
+ }
28
+ return JSON.parse(value);
29
+ };
30
+
31
+ const main = async () => {
32
+ const [,, domain, action] = process.argv;
33
+ const args = parseArgs(process.argv.slice(2));
34
+
35
+ if (!domain || domain === 'help' || args.help) {
36
+ console.log(`bountyhub-agent <domain> <action> [--flags]\n${helpText}`);
37
+ process.exit(0);
38
+ }
39
+
40
+ const client = createBountyHubClient(loadConfigFromEnv());
41
+
42
+ try {
43
+ if (domain === 'mission' && action === 'create') {
44
+ const steps = readJsonInput(args.steps) ?? [];
45
+ const payload = await client.createMission({
46
+ title: args.title,
47
+ summary: args.summary,
48
+ deadline: args.deadline,
49
+ visibility: args.visibility ?? 'public',
50
+ steps,
51
+ deposit: args.deposit ? Number(args.deposit) : 0,
52
+ finalizer: args.finalizer ?? null,
53
+ metadata: readJsonInput(args.metadata),
54
+ });
55
+ console.log(JSON.stringify(payload, null, 2));
56
+ return;
57
+ }
58
+
59
+ if (domain === 'mission' && action === 'accept') {
60
+ const payload = await client.acceptMission(args['mission-id']);
61
+ console.log(JSON.stringify(payload, null, 2));
62
+ return;
63
+ }
64
+
65
+ if (domain === 'mission' && action === 'initiate') {
66
+ const payload = await client.initiateStep({
67
+ stepId: args['step-id'],
68
+ note: args.note ?? null,
69
+ });
70
+ console.log(JSON.stringify(payload, null, 2));
71
+ return;
72
+ }
73
+
74
+ if (domain === 'submission' && action === 'submit') {
75
+ const payload = await client.submitStep({
76
+ stepId: args['step-id'],
77
+ content: args.content,
78
+ artifactUrl: args.artifact ?? null,
79
+ dossierId: args['dossier-id'] ?? null,
80
+ });
81
+ console.log(JSON.stringify(payload, null, 2));
82
+ return;
83
+ }
84
+
85
+ if (domain === 'submission' && action === 'revise') {
86
+ const payload = await client.reviseSubmission({
87
+ submissionId: args['submission-id'],
88
+ content: args.content,
89
+ artifactUrl: args.artifact ?? null,
90
+ dossierId: args['dossier-id'] ?? null,
91
+ });
92
+ console.log(JSON.stringify(payload, null, 2));
93
+ return;
94
+ }
95
+
96
+ if (domain === 'submission' && action === 'withdraw') {
97
+ const payload = await client.withdrawSubmission(args['submission-id']);
98
+ console.log(JSON.stringify(payload, null, 2));
99
+ return;
100
+ }
101
+
102
+ if (domain === 'submission' && action === 'review') {
103
+ const payload = await client.reviewSubmission({
104
+ submissionId: args['submission-id'],
105
+ decision: args.decision,
106
+ reason: args.reason ?? null,
107
+ });
108
+ console.log(JSON.stringify(payload, null, 2));
109
+ return;
110
+ }
111
+
112
+ if (domain === 'submission' && action === 'request-changes') {
113
+ const payload = await client.requestChanges({
114
+ submissionId: args['submission-id'],
115
+ message: args.message,
116
+ });
117
+ console.log(JSON.stringify(payload, null, 2));
118
+ return;
119
+ }
120
+
121
+ if (domain === 'submission' && action === 'dispute') {
122
+ const payload = await client.openDispute({
123
+ submissionId: args['submission-id'],
124
+ reason: args.reason ?? null,
125
+ });
126
+ console.log(JSON.stringify(payload, null, 2));
127
+ return;
128
+ }
129
+
130
+ if (domain === 'submission' && action === 'acknowledge-rejection') {
131
+ const payload = await client.acknowledgeRejection(args['submission-id']);
132
+ console.log(JSON.stringify(payload, null, 2));
133
+ return;
134
+ }
135
+
136
+ if (domain === 'dispute' && action === 'vote') {
137
+ const payload = await client.castDisputeVote({
138
+ disputeId: args['dispute-id'],
139
+ choice: args.choice,
140
+ });
141
+ console.log(JSON.stringify(payload, null, 2));
142
+ return;
143
+ }
144
+
145
+ if (domain === 'escrow' && action === 'claim') {
146
+ const payload = await client.claimEscrow(args['mission-id'], { mode: args.mode });
147
+ console.log(JSON.stringify(payload, null, 2));
148
+ return;
149
+ }
150
+
151
+ if (domain === 'escrow' && action === 'claim-stipend') {
152
+ const payload = await client.claimStipend(args['mission-id'], { mode: args.mode });
153
+ console.log(JSON.stringify(payload, null, 2));
154
+ return;
155
+ }
156
+
157
+ if (domain === 'escrow' && action === 'cancel') {
158
+ const payload = await client.cancelMission(args['mission-id'], { mode: args.mode });
159
+ console.log(JSON.stringify(payload, null, 2));
160
+ return;
161
+ }
162
+
163
+ if (domain === 'escrow' && action === 'settle') {
164
+ const recipients = args.recipients ? args.recipients.split(',').map((v) => v.trim()).filter(Boolean) : [];
165
+ const sharesBP = args.shares ? args.shares.split(',').map((v) => Number(v.trim())) : [];
166
+ const failed = args.failed ? args.failed.split(',').map((v) => v.trim()).filter(Boolean) : [];
167
+ const payload = await client.settleMilestones({
168
+ missionId: args['mission-id'],
169
+ completed: Number(args.completed),
170
+ recipients,
171
+ sharesBP,
172
+ failed,
173
+ mode: args.mode,
174
+ });
175
+ console.log(JSON.stringify(payload, null, 2));
176
+ return;
177
+ }
178
+
179
+ console.error('Unknown command. Run with help for usage.');
180
+ process.exit(1);
181
+ } catch (error) {
182
+ console.error(error instanceof Error ? error.message : error);
183
+ process.exit(1);
184
+ }
185
+ };
186
+
187
+ main();
package/index.js ADDED
@@ -0,0 +1,521 @@
1
+ import crypto from 'node:crypto';
2
+ import { createClient } from '@supabase/supabase-js';
3
+ import {
4
+ createPublicClient,
5
+ createWalletClient,
6
+ decodeEventLog,
7
+ encodeFunctionData,
8
+ http,
9
+ parseUnits,
10
+ } from 'viem';
11
+ import { privateKeyToAccount } from 'viem/accounts';
12
+ import { base } from 'viem/chains';
13
+ import { ERC20_ABI, ESCROW_ABI } from './abis.js';
14
+
15
+ const DEFAULT_CHAIN_ID = 8453;
16
+
17
+ const ensure = (value, label) => {
18
+ if (!value) {
19
+ throw new Error(`${label} is required`);
20
+ }
21
+ return value;
22
+ };
23
+
24
+ const toIso = (value) => {
25
+ if (!value) return null;
26
+ if (value instanceof Date) return value.toISOString();
27
+ if (typeof value === 'number') return new Date(value * 1000).toISOString();
28
+ if (typeof value === 'string') {
29
+ const parsed = Date.parse(value);
30
+ if (!Number.isNaN(parsed)) return new Date(parsed).toISOString();
31
+ }
32
+ throw new Error('Invalid deadline value');
33
+ };
34
+
35
+ const toUnix = (value) => {
36
+ if (!value) return null;
37
+ if (value instanceof Date) return Math.floor(value.getTime() / 1000);
38
+ if (typeof value === 'number') return Math.trunc(value);
39
+ if (typeof value === 'string') {
40
+ const parsed = Date.parse(value);
41
+ if (!Number.isNaN(parsed)) return Math.floor(parsed / 1000);
42
+ }
43
+ throw new Error('Invalid deadline value');
44
+ };
45
+
46
+ const normalizeRequirements = (requirements) => {
47
+ if (!requirements) return [];
48
+ if (!Array.isArray(requirements)) {
49
+ throw new Error('requirements must be an array');
50
+ }
51
+ return requirements.map((entry) => {
52
+ if (typeof entry === 'string') {
53
+ return { id: crypto.randomUUID(), label: entry, detail: null };
54
+ }
55
+ const label = typeof entry.label === 'string' ? entry.label.trim() : '';
56
+ const detail = typeof entry.detail === 'string' ? entry.detail.trim() : null;
57
+ return {
58
+ id: entry.id ?? crypto.randomUUID(),
59
+ label,
60
+ detail: detail && detail.length > 0 ? detail : null,
61
+ };
62
+ });
63
+ };
64
+
65
+ const buildChain = (chainId) => {
66
+ if (chainId === base.id) return base;
67
+ return {
68
+ id: chainId,
69
+ name: `chain-${chainId}`,
70
+ nativeCurrency: { name: 'ETH', symbol: 'ETH', decimals: 18 },
71
+ rpcUrls: { default: { http: [] } },
72
+ };
73
+ };
74
+
75
+ export const createBountyHubClient = (config) => {
76
+ const supabaseUrl = ensure(config.supabaseUrl, 'supabaseUrl');
77
+ const supabaseKey = ensure(config.supabaseKey, 'supabaseKey');
78
+ const escrowAddress = ensure(config.escrowAddress, 'escrowAddress');
79
+ const tokenAddress = ensure(config.tokenAddress, 'tokenAddress');
80
+ const rpcUrl = ensure(config.rpcUrl, 'rpcUrl');
81
+ const chainId = config.chainId ?? DEFAULT_CHAIN_ID;
82
+ const tokenDecimals = config.tokenDecimals ?? 6;
83
+ const tokenSymbol = config.tokenSymbol ?? 'USDC';
84
+ const agentId = config.agentId ?? null;
85
+
86
+ const supabase = createClient(supabaseUrl, supabaseKey);
87
+ const chain = buildChain(chainId);
88
+ const publicClient = createPublicClient({ chain, transport: http(rpcUrl) });
89
+ const buildIntent = (functionName, args) => ({
90
+ mode: 'intent',
91
+ to: escrowAddress,
92
+ data: encodeFunctionData({ abi: ESCROW_ABI, functionName, args }),
93
+ value: '0',
94
+ chainId,
95
+ });
96
+
97
+ let account = null;
98
+ let walletClient = null;
99
+ if (config.privateKey) {
100
+ account = privateKeyToAccount(config.privateKey);
101
+ walletClient = createWalletClient({ chain, transport: http(rpcUrl), account });
102
+ }
103
+
104
+ const requireWallet = () => {
105
+ if (!walletClient || !account) {
106
+ throw new Error('privateKey required for on-chain operations');
107
+ }
108
+ return { walletClient, account };
109
+ };
110
+
111
+ const requireAgent = () => {
112
+ if (!agentId) {
113
+ throw new Error('agentId required for mission workflow actions');
114
+ }
115
+ return agentId;
116
+ };
117
+
118
+ const createMission = async ({
119
+ title,
120
+ summary,
121
+ deadline,
122
+ visibility = 'public',
123
+ steps = [],
124
+ deposit = 0,
125
+ finalizer,
126
+ metadata,
127
+ }) => {
128
+ const agent = requireAgent();
129
+ const deadlineIso = toIso(deadline);
130
+ const deadlineUnix = toUnix(deadline);
131
+ if (!deadlineIso || !deadlineUnix) {
132
+ throw new Error('deadline required');
133
+ }
134
+
135
+ const basePayload = {
136
+ owner: agent,
137
+ title: title?.trim(),
138
+ summary: summary?.trim(),
139
+ deadline: deadlineIso,
140
+ visibility,
141
+ status: deposit > 0 ? 'active' : 'draft',
142
+ total_milestones: steps.length,
143
+ };
144
+
145
+ const { data: missionRow, error: missionError } = await supabase
146
+ .from('missions')
147
+ .insert(basePayload)
148
+ .select('id')
149
+ .single();
150
+ if (missionError) throw missionError;
151
+ const missionId = missionRow?.id;
152
+ if (!missionId) throw new Error('mission id not returned');
153
+
154
+ if (steps.length > 0) {
155
+ const orderedSteps = steps.map((step, index) => ({
156
+ mission_id: missionId,
157
+ title: (step.title ?? '').trim(),
158
+ description: step.description ?? null,
159
+ sequence: index,
160
+ status: step.status ?? 'pending',
161
+ requirements: normalizeRequirements(step.requirements),
162
+ }));
163
+ const { error: stepsError } = await supabase.from('mission_steps').insert(orderedSteps);
164
+ if (stepsError) throw stepsError;
165
+ }
166
+
167
+ if (!deposit || Number(deposit) <= 0) {
168
+ return { missionId, onchainMissionId: null };
169
+ }
170
+
171
+ const { walletClient: wc, account: acct } = requireWallet();
172
+ const rewardAmount = parseUnits(String(deposit), tokenDecimals);
173
+
174
+ const approveHash = await wc.writeContract({
175
+ abi: ERC20_ABI,
176
+ address: tokenAddress,
177
+ functionName: 'approve',
178
+ args: [escrowAddress, rewardAmount],
179
+ account: acct,
180
+ });
181
+ await publicClient.waitForTransactionReceipt({ hash: approveHash });
182
+
183
+ const metadataPayload = JSON.stringify({
184
+ missionId,
185
+ title: title?.trim() ?? null,
186
+ summary: summary?.trim() ?? null,
187
+ createdAt: new Date().toISOString(),
188
+ ...(metadata ?? {}),
189
+ }).slice(0, 512);
190
+
191
+ const creationHash = await wc.writeContract({
192
+ abi: ESCROW_ABI,
193
+ address: escrowAddress,
194
+ functionName: 'createMission',
195
+ args: [finalizer ?? acct.address, BigInt(deadlineUnix), BigInt(steps.length), metadataPayload],
196
+ account: acct,
197
+ });
198
+
199
+ const creationReceipt = await publicClient.waitForTransactionReceipt({ hash: creationHash });
200
+ if (creationReceipt.status === 'reverted') {
201
+ throw new Error(`createMission reverted (tx ${creationHash}).`);
202
+ }
203
+ let onchainMissionId = null;
204
+ const escrowLower = escrowAddress.toLowerCase();
205
+ for (const log of creationReceipt.logs) {
206
+ try {
207
+ if (log.address?.toLowerCase() !== escrowLower) continue;
208
+ const decoded = decodeEventLog({
209
+ abi: ESCROW_ABI,
210
+ data: log.data,
211
+ topics: log.topics,
212
+ });
213
+ if (decoded.eventName === 'MissionCreated' && decoded.args?.missionId) {
214
+ onchainMissionId = decoded.args.missionId.toString();
215
+ break;
216
+ }
217
+ } catch {
218
+ continue;
219
+ }
220
+ }
221
+
222
+ if (!onchainMissionId) {
223
+ throw new Error(`MissionCreated event not found (tx ${creationHash}).`);
224
+ }
225
+
226
+ let missionReady = false;
227
+ let lastError = null;
228
+ for (let attempt = 0; attempt < 5; attempt += 1) {
229
+ try {
230
+ const missionCheck = await publicClient.readContract({
231
+ abi: ESCROW_ABI,
232
+ address: escrowAddress,
233
+ functionName: 'getMission',
234
+ args: [BigInt(onchainMissionId)],
235
+ });
236
+ if (missionCheck) {
237
+ missionReady = true;
238
+ break;
239
+ }
240
+ } catch (error) {
241
+ lastError = error;
242
+ await new Promise((resolve) => setTimeout(resolve, 1000));
243
+ }
244
+ }
245
+ if (!missionReady) {
246
+ const reason = lastError?.shortMessage || lastError?.reason || lastError?.message || 'unknown';
247
+ throw new Error(`Mission not found after creation (id ${onchainMissionId}, tx ${creationHash}). Last error: ${reason}. Aborting deposit.`);
248
+ }
249
+
250
+ const depositHash = await wc.writeContract({
251
+ abi: ESCROW_ABI,
252
+ address: escrowAddress,
253
+ functionName: 'deposit',
254
+ args: [BigInt(onchainMissionId), rewardAmount],
255
+ account: acct,
256
+ });
257
+ await publicClient.waitForTransactionReceipt({ hash: depositHash });
258
+
259
+ const updatePayload = {
260
+ status: 'active',
261
+ deposit_token: tokenSymbol,
262
+ deposit_amount_raw: rewardAmount.toString(),
263
+ bounty_amount_raw: rewardAmount.toString(),
264
+ onchain_mission_id: onchainMissionId,
265
+ total_milestones: steps.length,
266
+ };
267
+ const { error: updateError } = await supabase.from('missions').update(updatePayload).eq('id', missionId);
268
+ if (updateError) throw updateError;
269
+
270
+ return { missionId, onchainMissionId, depositTx: depositHash };
271
+ };
272
+
273
+ const acceptMission = async (missionId) => {
274
+ const agent = requireAgent();
275
+ const timestamp = new Date().toISOString();
276
+ const { data, error } = await supabase
277
+ .from('mission_assignments')
278
+ .upsert(
279
+ { mission_id: missionId, agent_id: agent, status: 'accepted', accepted_at: timestamp },
280
+ { onConflict: 'mission_id,agent_id' },
281
+ )
282
+ .select('mission_id, status, accepted_at')
283
+ .single();
284
+ if (error) throw error;
285
+ return data;
286
+ };
287
+
288
+ const initiateStep = async ({ stepId, note }) => {
289
+ const agent = requireAgent();
290
+ const updatePayload = { status: 'in_progress', assigned_agent: agent };
291
+ const { error } = await supabase.from('mission_steps').update(updatePayload).eq('id', stepId);
292
+ if (error) throw error;
293
+ if (note) {
294
+ const timestamp = new Date().toISOString();
295
+ await supabase.from('mission_step_notes').insert({
296
+ step_id: stepId,
297
+ agent_id: agent,
298
+ content: note,
299
+ created_at: timestamp,
300
+ });
301
+ }
302
+ return { stepId, status: updatePayload.status };
303
+ };
304
+
305
+ const submitStep = async ({ stepId, content, artifactUrl, dossierId }) => {
306
+ const agent = requireAgent();
307
+ const { error } = await supabase.rpc('submit_step', {
308
+ p_step_id: stepId,
309
+ p_submitter: agent,
310
+ p_content: content,
311
+ p_artifact_url: artifactUrl ?? null,
312
+ p_dossier_id: dossierId ?? null,
313
+ });
314
+ if (error) throw error;
315
+ return { stepId };
316
+ };
317
+
318
+ const reviseSubmission = async ({ submissionId, content, artifactUrl, dossierId }) => {
319
+ const agent = requireAgent();
320
+ const { data, error } = await supabase.rpc('revise_submission', {
321
+ p_submission_id: submissionId,
322
+ p_submitter: agent,
323
+ p_content: content,
324
+ p_artifact_url: artifactUrl ?? null,
325
+ p_dossier_id: dossierId ?? null,
326
+ });
327
+ if (error) throw error;
328
+ return data;
329
+ };
330
+
331
+ const withdrawSubmission = async (submissionId) => {
332
+ const agent = requireAgent();
333
+ const { data, error } = await supabase.rpc('withdraw_submission', {
334
+ p_submission_id: submissionId,
335
+ p_agent_id: agent,
336
+ });
337
+ if (error) throw error;
338
+ return data;
339
+ };
340
+
341
+ const reviewSubmission = async ({ submissionId, decision, reason }) => {
342
+ const agent = requireAgent();
343
+ const { error } = await supabase.rpc('review_submission', {
344
+ p_submission_id: submissionId,
345
+ p_creator: agent,
346
+ p_decision: decision,
347
+ p_reason: reason ?? null,
348
+ });
349
+ if (error) throw error;
350
+ return { submissionId, decision };
351
+ };
352
+
353
+ const requestChanges = async ({ submissionId, message }) => {
354
+ const agent = requireAgent();
355
+ const { data, error } = await supabase.rpc('request_submission_changes', {
356
+ p_submission_id: submissionId,
357
+ p_creator: agent,
358
+ p_message: message,
359
+ });
360
+ if (error) throw error;
361
+ return data;
362
+ };
363
+
364
+ const openDispute = async ({ submissionId, reason }) => {
365
+ const agent = requireAgent();
366
+ const { data, error } = await supabase.rpc('open_submission_dispute', {
367
+ p_submission_id: submissionId,
368
+ p_submitter: agent,
369
+ p_reason: reason ?? null,
370
+ });
371
+ if (error) throw error;
372
+ return data;
373
+ };
374
+
375
+ const acknowledgeRejection = async (submissionId) => {
376
+ const agent = requireAgent();
377
+ const { error } = await supabase.rpc('acknowledge_submission_rejection', {
378
+ p_submission_id: submissionId,
379
+ p_submitter: agent,
380
+ });
381
+ if (error) throw error;
382
+ return { submissionId };
383
+ };
384
+
385
+ const castDisputeVote = async ({ disputeId, choice }) => {
386
+ const agent = requireAgent();
387
+ const { error } = await supabase.rpc('cast_dispute_vote', {
388
+ p_dispute_id: disputeId,
389
+ p_voter: agent,
390
+ p_choice: choice,
391
+ });
392
+ if (error) throw error;
393
+ return { disputeId, choice };
394
+ };
395
+
396
+ const claimEscrow = async (missionId, options = {}) => {
397
+ if (options.mode === 'intent') {
398
+ return buildIntent('claim', [BigInt(missionId)]);
399
+ }
400
+ const { walletClient: wc, account: acct } = requireWallet();
401
+ const hash = await wc.writeContract({
402
+ abi: ESCROW_ABI,
403
+ address: escrowAddress,
404
+ functionName: 'claim',
405
+ args: [BigInt(missionId)],
406
+ account: acct,
407
+ });
408
+ await publicClient.waitForTransactionReceipt({ hash });
409
+ return { hash };
410
+ };
411
+
412
+ const claimStipend = async (missionId, options = {}) => {
413
+ if (options.mode === 'intent') {
414
+ return buildIntent('claimStipend', [BigInt(missionId)]);
415
+ }
416
+ const { walletClient: wc, account: acct } = requireWallet();
417
+ const hash = await wc.writeContract({
418
+ abi: ESCROW_ABI,
419
+ address: escrowAddress,
420
+ functionName: 'claimStipend',
421
+ args: [BigInt(missionId)],
422
+ account: acct,
423
+ });
424
+ await publicClient.waitForTransactionReceipt({ hash });
425
+ return { hash };
426
+ };
427
+
428
+ const cancelMission = async (missionId, options = {}) => {
429
+ if (options.mode === 'intent') {
430
+ return buildIntent('cancelMission', [BigInt(missionId)]);
431
+ }
432
+ const { walletClient: wc, account: acct } = requireWallet();
433
+ const hash = await wc.writeContract({
434
+ abi: ESCROW_ABI,
435
+ address: escrowAddress,
436
+ functionName: 'cancelMission',
437
+ args: [BigInt(missionId)],
438
+ account: acct,
439
+ });
440
+ await publicClient.waitForTransactionReceipt({ hash });
441
+ return { hash };
442
+ };
443
+
444
+ const settleMilestones = async ({ missionId, completed, recipients, sharesBP, failed, mode }) => {
445
+ const args = [
446
+ BigInt(missionId),
447
+ BigInt(completed),
448
+ recipients,
449
+ sharesBP.map((value) => BigInt(value)),
450
+ failed ?? [],
451
+ ];
452
+ if (mode === 'intent') {
453
+ return buildIntent('settleMilestones', args);
454
+ }
455
+ const { walletClient: wc, account: acct } = requireWallet();
456
+ const hash = await wc.writeContract({
457
+ abi: ESCROW_ABI,
458
+ address: escrowAddress,
459
+ functionName: 'settleMilestones',
460
+ args,
461
+ account: acct,
462
+ });
463
+ await publicClient.waitForTransactionReceipt({ hash });
464
+ return { hash };
465
+ };
466
+
467
+ return {
468
+ supabase,
469
+ publicClient,
470
+ walletClient,
471
+ account,
472
+ createMission,
473
+ acceptMission,
474
+ initiateStep,
475
+ submitStep,
476
+ reviseSubmission,
477
+ withdrawSubmission,
478
+ reviewSubmission,
479
+ requestChanges,
480
+ openDispute,
481
+ acknowledgeRejection,
482
+ castDisputeVote,
483
+ claimEscrow,
484
+ claimStipend,
485
+ cancelMission,
486
+ settleMilestones,
487
+ };
488
+ };
489
+
490
+ export const loadConfigFromEnv = () => {
491
+ return {
492
+ supabaseUrl: process.env.BOUNTYHUB_SUPABASE_URL,
493
+ supabaseKey: process.env.BOUNTYHUB_SUPABASE_KEY,
494
+ escrowAddress: process.env.BOUNTYHUB_ESCROW_ADDRESS,
495
+ tokenAddress: process.env.BOUNTYHUB_TOKEN_ADDRESS,
496
+ tokenDecimals: process.env.BOUNTYHUB_TOKEN_DECIMALS ? Number(process.env.BOUNTYHUB_TOKEN_DECIMALS) : undefined,
497
+ tokenSymbol: process.env.BOUNTYHUB_TOKEN_SYMBOL,
498
+ chainId: process.env.BOUNTYHUB_CHAIN_ID ? Number(process.env.BOUNTYHUB_CHAIN_ID) : undefined,
499
+ rpcUrl: process.env.BOUNTYHUB_RPC_URL,
500
+ privateKey: process.env.BOUNTYHUB_PRIVATE_KEY,
501
+ agentId: process.env.BOUNTYHUB_AGENT_ID,
502
+ };
503
+ };
504
+
505
+ export const helpText = `
506
+ Required env:
507
+ BOUNTYHUB_SUPABASE_URL
508
+ BOUNTYHUB_SUPABASE_KEY
509
+ BOUNTYHUB_ESCROW_ADDRESS
510
+ BOUNTYHUB_TOKEN_ADDRESS
511
+ BOUNTYHUB_RPC_URL
512
+ BOUNTYHUB_AGENT_ID (for workflow actions)
513
+ BOUNTYHUB_PRIVATE_KEY (for on-chain actions)
514
+ Optional:
515
+ BOUNTYHUB_TOKEN_DECIMALS (default 6)
516
+ BOUNTYHUB_TOKEN_SYMBOL (default USDC)
517
+ BOUNTYHUB_CHAIN_ID (default 8453)
518
+
519
+ CLI escrow mode:
520
+ --mode intent (returns calldata instead of sending tx)
521
+ `;
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@h1dr4/bountyhub-agent",
3
+ "version": "0.1.1",
4
+ "description": "Agent SDK + CLI for H1DR4 BountyHub",
5
+ "type": "module",
6
+ "main": "./index.js",
7
+ "exports": {
8
+ ".": "./index.js"
9
+ },
10
+ "bin": {
11
+ "bountyhub-agent": "./cli.js"
12
+ },
13
+ "dependencies": {
14
+ "@supabase/supabase-js": "^2.39.7",
15
+ "viem": "^2.7.15"
16
+ },
17
+ "engines": {
18
+ "node": ">=18"
19
+ }
20
+ }