spendos 0.1.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.
Files changed (90) hide show
  1. package/.dockerignore +4 -0
  2. package/.env.example +30 -0
  3. package/AGENTS.md +212 -0
  4. package/BOOTSTRAP.md +55 -0
  5. package/Dockerfile +52 -0
  6. package/HEARTBEAT.md +7 -0
  7. package/IDENTITY.md +23 -0
  8. package/LICENSE +21 -0
  9. package/README.md +162 -0
  10. package/SOUL.md +202 -0
  11. package/SUBMISSION.md +128 -0
  12. package/TOOLS.md +40 -0
  13. package/USER.md +17 -0
  14. package/acp-seller/bin/acp.ts +807 -0
  15. package/acp-seller/config.json +34 -0
  16. package/acp-seller/package.json +55 -0
  17. package/acp-seller/src/commands/agent.ts +328 -0
  18. package/acp-seller/src/commands/bounty.ts +1189 -0
  19. package/acp-seller/src/commands/deploy.ts +414 -0
  20. package/acp-seller/src/commands/job.ts +217 -0
  21. package/acp-seller/src/commands/profile.ts +71 -0
  22. package/acp-seller/src/commands/resource.ts +91 -0
  23. package/acp-seller/src/commands/search.ts +327 -0
  24. package/acp-seller/src/commands/sell.ts +883 -0
  25. package/acp-seller/src/commands/serve.ts +258 -0
  26. package/acp-seller/src/commands/setup.ts +399 -0
  27. package/acp-seller/src/commands/token.ts +88 -0
  28. package/acp-seller/src/commands/wallet.ts +123 -0
  29. package/acp-seller/src/lib/api.ts +118 -0
  30. package/acp-seller/src/lib/auth.ts +291 -0
  31. package/acp-seller/src/lib/bounty.ts +257 -0
  32. package/acp-seller/src/lib/client.ts +42 -0
  33. package/acp-seller/src/lib/config.ts +240 -0
  34. package/acp-seller/src/lib/open.ts +41 -0
  35. package/acp-seller/src/lib/openclawCron.ts +138 -0
  36. package/acp-seller/src/lib/output.ts +104 -0
  37. package/acp-seller/src/lib/wallet.ts +81 -0
  38. package/acp-seller/src/seller/offerings/_shared/preTransactionScan.ts +127 -0
  39. package/acp-seller/src/seller/offerings/canonical-catalog.ts +221 -0
  40. package/acp-seller/src/seller/offerings/spendos/spendos_summarize_url/handlers.ts +20 -0
  41. package/acp-seller/src/seller/offerings/spendos/spendos_summarize_url/offering.json +18 -0
  42. package/acp-seller/src/seller/offerings/spendos/spendos_translate/handlers.ts +21 -0
  43. package/acp-seller/src/seller/offerings/spendos/spendos_translate/offering.json +22 -0
  44. package/acp-seller/src/seller/offerings/spendos/spendos_tweet_gen/handlers.ts +20 -0
  45. package/acp-seller/src/seller/offerings/spendos/spendos_tweet_gen/offering.json +18 -0
  46. package/acp-seller/src/seller/runtime/acpSocket.ts +413 -0
  47. package/acp-seller/src/seller/runtime/logger.ts +36 -0
  48. package/acp-seller/src/seller/runtime/offeringTypes.ts +52 -0
  49. package/acp-seller/src/seller/runtime/offerings.ts +277 -0
  50. package/acp-seller/src/seller/runtime/paymentVerification.test.ts +207 -0
  51. package/acp-seller/src/seller/runtime/paymentVerification.ts +363 -0
  52. package/acp-seller/src/seller/runtime/seller.onchain.test.ts +220 -0
  53. package/acp-seller/src/seller/runtime/seller.test.ts +823 -0
  54. package/acp-seller/src/seller/runtime/seller.ts +1041 -0
  55. package/acp-seller/src/seller/runtime/sellerApi.ts +71 -0
  56. package/acp-seller/src/seller/runtime/startup.ts +270 -0
  57. package/acp-seller/src/seller/runtime/types.ts +62 -0
  58. package/acp-seller/tsconfig.json +20 -0
  59. package/bin/spendos.js +23 -0
  60. package/contracts/SpendOSAudit.sol +29 -0
  61. package/dist/mcp-server.mjs +153 -0
  62. package/jobs/translate.json +7 -0
  63. package/jobs/tweet-gen.json +7 -0
  64. package/openclaw.json +41 -0
  65. package/package.json +49 -0
  66. package/plugins/spendos-events/index.ts +78 -0
  67. package/plugins/spendos-events/package.json +14 -0
  68. package/policies/enforce-bounds.mjs +71 -0
  69. package/public/index.html +509 -0
  70. package/public/landing.html +241 -0
  71. package/railway.json +12 -0
  72. package/railway.toml +12 -0
  73. package/scripts/deploy.ts +48 -0
  74. package/scripts/test-x402-mainnet.ts +30 -0
  75. package/scripts/xmtp-listener.ts +61 -0
  76. package/setup.sh +278 -0
  77. package/skills/spendos/skill.md +26 -0
  78. package/src/agent.ts +152 -0
  79. package/src/audit.ts +166 -0
  80. package/src/governance.ts +367 -0
  81. package/src/job-registry.ts +306 -0
  82. package/src/mcp-public.ts +145 -0
  83. package/src/mcp-server.ts +171 -0
  84. package/src/opportunity-scanner.ts +138 -0
  85. package/src/server.ts +870 -0
  86. package/src/venice-x402.ts +234 -0
  87. package/src/xmtp.ts +109 -0
  88. package/src/zerion.ts +58 -0
  89. package/start.sh +168 -0
  90. package/tsconfig.json +14 -0
@@ -0,0 +1,91 @@
1
+ // =============================================================================
2
+ // acp resource query <url> [--params '<json>'] — Query a resource by URL
3
+ // =============================================================================
4
+
5
+ import axios from "axios";
6
+ import * as output from "../lib/output.js";
7
+
8
+ export async function query(
9
+ url: string,
10
+ params?: Record<string, any>,
11
+ ): Promise<void> {
12
+ if (!url) {
13
+ output.fatal("Usage: acp resource query <url> [--params '<json>']");
14
+ }
15
+
16
+ // Validate URL format
17
+ try {
18
+ new URL(url);
19
+ } catch {
20
+ output.fatal(`Invalid URL: ${url}`);
21
+ }
22
+
23
+ try {
24
+ // Make HTTP request to resource URL
25
+ output.log(`\nQuerying resource at: ${url}`);
26
+ if (params && Object.keys(params).length > 0) {
27
+ output.log(` With params: ${JSON.stringify(params, null, 2)}\n`);
28
+ } else {
29
+ output.log("");
30
+ }
31
+
32
+ let response;
33
+ try {
34
+ // Always use GET request, params as query string
35
+ if (params && Object.keys(params).length > 0) {
36
+ // Build query string from params
37
+ const queryString = new URLSearchParams();
38
+ for (const [key, value] of Object.entries(params)) {
39
+ if (value !== null && value !== undefined) {
40
+ queryString.append(key, String(value));
41
+ }
42
+ }
43
+ const urlWithParams = url.includes("?")
44
+ ? `${url}&${queryString.toString()}`
45
+ : `${url}?${queryString.toString()}`;
46
+ response = await axios.get(urlWithParams);
47
+ } else {
48
+ response = await axios.get(url);
49
+ }
50
+ } catch (httpError: any) {
51
+ if (httpError.response) {
52
+ // Server responded with error status
53
+ const errorMsg = httpError.response.data
54
+ ? JSON.stringify(httpError.response.data, null, 2)
55
+ : httpError.response.statusText;
56
+ output.fatal(
57
+ `Resource query failed: ${httpError.response.status} ${httpError.response.statusText}\n${errorMsg}`,
58
+ );
59
+ } else {
60
+ output.fatal(
61
+ `Resource query failed: ${
62
+ httpError instanceof Error ? httpError.message : String(httpError)
63
+ }`,
64
+ );
65
+ }
66
+ }
67
+
68
+ const responseData = response.data;
69
+
70
+ output.output(responseData, (data) => {
71
+ output.heading(`Resource Query Result`);
72
+ output.log(`\n URL: ${url}`);
73
+ output.log(`\n Response:`);
74
+ if (typeof data === "string") {
75
+ output.log(` ${data}`);
76
+ } else {
77
+ output.log(
78
+ ` ${JSON.stringify(data, null, 2)
79
+ .split("\n")
80
+ .map((line, i) => (i === 0 ? line : ` ${line}`))
81
+ .join("\n")}`,
82
+ );
83
+ }
84
+ output.log("");
85
+ });
86
+ } catch (e) {
87
+ output.fatal(
88
+ `Resource query failed: ${e instanceof Error ? e.message : String(e)}`,
89
+ );
90
+ }
91
+ }
@@ -0,0 +1,327 @@
1
+ // =============================================================================
2
+ // acp browse <query> — Search agents with filters and configurations
3
+ // =============================================================================
4
+
5
+ import axios from "axios";
6
+ import * as output from "../lib/output.js";
7
+
8
+ const SEARCH_URL =
9
+ process.env.SEARCH_URL || "https://acpx.virtuals.io/api/agents/v5/search";
10
+
11
+ // -- Types --
12
+
13
+ export interface SearchOptions {
14
+ mode?: "hybrid" | "vector" | "keyword";
15
+ contains?: string;
16
+ match?: "all" | "any";
17
+ similarityCutoff?: number;
18
+ sparseCutoff?: number;
19
+ topK?: number;
20
+ }
21
+
22
+ interface AgentMetrics {
23
+ successfulJobCount: number | null;
24
+ successRate: number | null;
25
+ uniqueBuyerCount: number | null;
26
+ minsFromLastOnlineTime: number | null;
27
+ isOnline: boolean;
28
+ }
29
+
30
+ interface AgentJob {
31
+ id: number;
32
+ name: string;
33
+ description: string;
34
+ type: string;
35
+ price: number;
36
+ priceV2: { type: string; value: number };
37
+ requiredFunds: boolean;
38
+ slaMinutes: number;
39
+ requirement: Record<string, unknown>;
40
+ deliverable: Record<string, unknown>;
41
+ }
42
+
43
+ interface AgentResource {
44
+ name: string;
45
+ description?: string;
46
+ url?: string;
47
+ params?: Record<string, unknown>;
48
+ [key: string]: unknown;
49
+ }
50
+
51
+ interface Agent {
52
+ id: number;
53
+ name: string;
54
+ description: string;
55
+ contractAddress: string;
56
+ walletAddress: string;
57
+ twitterHandle: string;
58
+ profilePic: string;
59
+ tokenAddress: string | null;
60
+ cluster: string | null;
61
+ category: string | null;
62
+ symbol: string | null;
63
+ virtualAgentId: number | null;
64
+ isVirtualAgent: boolean;
65
+ metrics: AgentMetrics;
66
+ jobs: AgentJob[];
67
+ resources: AgentResource[];
68
+ }
69
+
70
+ // -- Defaults (server-side, documented here for help text & summary) --
71
+
72
+ export const SEARCH_DEFAULTS = {
73
+ mode: "hybrid" as const,
74
+ similarityCutoff: 0.5,
75
+ sparseCutoff: 0.0,
76
+ match: "all" as const,
77
+ topK: 5,
78
+ };
79
+
80
+ // -- Friendly mode → API searchMode mapping --
81
+
82
+ const MODE_MAP: Record<string, string> = {
83
+ hybrid: "hybrid",
84
+ vector: "dense",
85
+ keyword: "sparse",
86
+ };
87
+
88
+ // -- Build query params --
89
+
90
+ function buildParams(
91
+ query: string,
92
+ opts: SearchOptions,
93
+ ): Record<string, string> {
94
+ const params: Record<string, string> = { query };
95
+ params.claw = "true";
96
+
97
+ // Search mode
98
+ if (opts.mode) {
99
+ const apiMode = MODE_MAP[opts.mode];
100
+ if (!apiMode) {
101
+ output.fatal(
102
+ `Invalid search mode "${opts.mode}". Use: hybrid, vector, keyword`,
103
+ );
104
+ }
105
+ params.searchMode = apiMode;
106
+ }
107
+
108
+ // String filters
109
+ if (opts.contains) params.fullTextFilter = opts.contains;
110
+ if (opts.match) params.fullTextMatch = opts.match;
111
+
112
+ // Cutoffs
113
+ params.similarityCutoff = String(
114
+ opts.similarityCutoff ?? SEARCH_DEFAULTS.similarityCutoff,
115
+ );
116
+ if (opts.sparseCutoff !== undefined)
117
+ params.sparseCutoff = String(opts.sparseCutoff);
118
+
119
+ // Result count
120
+ params.topK = String(opts.topK ?? SEARCH_DEFAULTS.topK);
121
+
122
+ return params;
123
+ }
124
+
125
+ // -- Table formatting --
126
+
127
+ function truncate(s: string, max: number): string {
128
+ return s.length > max ? s.slice(0, max - 1) + "…" : s;
129
+ }
130
+
131
+ function formatTable(agents: Agent[]): void {
132
+ const header = {
133
+ rank: "#",
134
+ name: "Name",
135
+ id: "ID",
136
+ category: "Category",
137
+ rate: "Success",
138
+ jobs: "Jobs",
139
+ buyers: "Buyers",
140
+ online: "Online",
141
+ };
142
+
143
+ // Column widths
144
+ const w = {
145
+ rank: 4,
146
+ name: 20,
147
+ id: 6,
148
+ category: 16,
149
+ rate: 9,
150
+ jobs: 6,
151
+ buyers: 8,
152
+ online: 6,
153
+ };
154
+
155
+ const row = (r: typeof header) =>
156
+ ` ${r.rank.toString().padStart(w.rank)} ` +
157
+ `${truncate(r.name, w.name).padEnd(w.name)} ` +
158
+ `${r.id.toString().padEnd(w.id)} ` +
159
+ `${truncate(r.category, w.category).padEnd(w.category)} ` +
160
+ `${r.rate.toString().padStart(w.rate)} ` +
161
+ `${r.jobs.toString().padStart(w.jobs)} ` +
162
+ `${r.buyers.toString().padStart(w.buyers)} ` +
163
+ `${r.online.toString().padEnd(w.online)}`;
164
+
165
+ // Header
166
+ output.log(output.colors.dim(row(header)));
167
+
168
+ // Rows
169
+ for (let i = 0; i < agents.length; i++) {
170
+ const a = agents[i];
171
+ output.log(
172
+ row({
173
+ rank: String(i + 1),
174
+ name: a.name,
175
+ id: String(a.id),
176
+ category: a.category ?? "-",
177
+ rate:
178
+ a.metrics.successRate != null
179
+ ? `${a.metrics.successRate.toFixed(1)}%`
180
+ : "-",
181
+ jobs:
182
+ a.metrics.successfulJobCount != null
183
+ ? String(a.metrics.successfulJobCount)
184
+ : "-",
185
+ buyers:
186
+ a.metrics.uniqueBuyerCount != null
187
+ ? String(a.metrics.uniqueBuyerCount)
188
+ : "-",
189
+ online: a.metrics.isOnline ? "Yes" : "No",
190
+ }),
191
+ );
192
+ }
193
+ }
194
+
195
+ // -- Detailed per-agent output (offerings + resources) --
196
+
197
+ function formatPrice(price: number, priceType?: string): string {
198
+ if (priceType === "percentage") return `${(price * 100).toFixed(1)}%`;
199
+ return `$${price} USDC`;
200
+ }
201
+
202
+ function formatDetails(agents: Agent[]): void {
203
+ for (const a of agents) {
204
+ output.log(`\n ${output.colors.bold(a.name)}`);
205
+ output.log(` Wallet: ${a.walletAddress}`);
206
+ if (a.description) {
207
+ output.log(` ${output.colors.dim(a.description)}`);
208
+ }
209
+
210
+ const jobs = a.jobs ?? [];
211
+ if (jobs.length > 0) {
212
+ output.log(" Offerings:");
213
+ for (const j of jobs) {
214
+ const fee = formatPrice(j.price, j.priceV2?.type);
215
+ const funds = j.requiredFunds ? " [requires funds]" : "";
216
+ output.log(` - ${j.name} (${fee}${funds})`);
217
+ if (j.description) {
218
+ output.log(` ${j.description}`);
219
+ }
220
+ if (j.requirement && Object.keys(j.requirement).length > 0) {
221
+ const req = JSON.stringify(j.requirement, null, 2)
222
+ .split("\n")
223
+ .join("\n ");
224
+ output.log(` Requirement: ${req}`);
225
+ }
226
+ }
227
+ }
228
+
229
+ const resources = a.resources ?? [];
230
+ if (resources.length > 0) {
231
+ output.log(" Resources:");
232
+ for (const r of resources) {
233
+ output.log(` - ${r.name}`);
234
+ if (r.description) {
235
+ output.log(` ${r.description}`);
236
+ }
237
+ if (r.url) {
238
+ output.log(` URL: ${r.url}`);
239
+ }
240
+ }
241
+ }
242
+ }
243
+ }
244
+
245
+ // -- Settings summary --
246
+
247
+ function formatSummary(opts: SearchOptions): string {
248
+ const parts: string[] = [];
249
+
250
+ // Mode
251
+ parts.push(`mode=${opts.mode ?? SEARCH_DEFAULTS.mode}`);
252
+
253
+ // Active filters
254
+ const filters: string[] = [];
255
+ if (opts.contains) {
256
+ const m = opts.match ?? SEARCH_DEFAULTS.match;
257
+ filters.push(`contains="${opts.contains}" (match=${m})`);
258
+ }
259
+ parts.push(filters.join(", "));
260
+
261
+ return parts.join(" · ");
262
+ }
263
+
264
+ // -- Main search function --
265
+
266
+ export async function search(
267
+ query: string,
268
+ opts: SearchOptions,
269
+ ): Promise<void> {
270
+ if (!query.trim()) {
271
+ output.fatal(
272
+ "Usage: acp browse <query>\n Run `acp browse --help` for all options.",
273
+ );
274
+ }
275
+
276
+ // Validate: --match requires --contains
277
+ if (opts.match && !opts.contains) {
278
+ output.fatal("--match requires --contains");
279
+ }
280
+
281
+ const params = buildParams(query, opts);
282
+
283
+ try {
284
+ const response = await axios.get<{ data: Agent[] }>(SEARCH_URL, { params });
285
+ const data = response.data?.data;
286
+
287
+ // Handle the known SQL-error quirk for empty results
288
+ if (!data || !Array.isArray(data) || data.length === 0) {
289
+ output.output([], () => {
290
+ output.log(`\n No agents found for "${query}".`);
291
+ output.log(
292
+ ` Try tweaking search parameters (\`acp browse --help\`) or run \`acp bounty create "${query}"\` to post a bounty.`,
293
+ );
294
+ output.log("");
295
+ });
296
+ return;
297
+ }
298
+
299
+ output.output(data, (agents: Agent[]) => {
300
+ output.heading(`Search results for "${query}"`);
301
+ output.log(output.colors.dim(` ${formatSummary(opts)}`));
302
+ output.log("");
303
+ formatTable(agents);
304
+ formatDetails(agents);
305
+ output.log(
306
+ output.colors.dim(
307
+ `\n ${agents.length} result${agents.length === 1 ? "" : "s"}`,
308
+ ),
309
+ );
310
+ output.log("");
311
+ });
312
+ } catch (e: unknown) {
313
+ // Handle the SQL-error quirk (empty WHERE IN ())
314
+ const msg = e instanceof Error ? e.message : String(e);
315
+ if (msg.includes("syntax") || msg.includes("SQL")) {
316
+ output.output([], () => {
317
+ output.log(`\n No agents found for "${query}".`);
318
+ output.log(
319
+ ` Try tweaking search parameters (\`acp browse --help\`) or run \`acp bounty create "${query}"\` to post a bounty.`,
320
+ );
321
+ output.log("");
322
+ });
323
+ return;
324
+ }
325
+ output.fatal(`Search failed: ${msg}`);
326
+ }
327
+ }