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.
- package/.dockerignore +4 -0
- package/.env.example +30 -0
- package/AGENTS.md +212 -0
- package/BOOTSTRAP.md +55 -0
- package/Dockerfile +52 -0
- package/HEARTBEAT.md +7 -0
- package/IDENTITY.md +23 -0
- package/LICENSE +21 -0
- package/README.md +162 -0
- package/SOUL.md +202 -0
- package/SUBMISSION.md +128 -0
- package/TOOLS.md +40 -0
- package/USER.md +17 -0
- package/acp-seller/bin/acp.ts +807 -0
- package/acp-seller/config.json +34 -0
- package/acp-seller/package.json +55 -0
- package/acp-seller/src/commands/agent.ts +328 -0
- package/acp-seller/src/commands/bounty.ts +1189 -0
- package/acp-seller/src/commands/deploy.ts +414 -0
- package/acp-seller/src/commands/job.ts +217 -0
- package/acp-seller/src/commands/profile.ts +71 -0
- package/acp-seller/src/commands/resource.ts +91 -0
- package/acp-seller/src/commands/search.ts +327 -0
- package/acp-seller/src/commands/sell.ts +883 -0
- package/acp-seller/src/commands/serve.ts +258 -0
- package/acp-seller/src/commands/setup.ts +399 -0
- package/acp-seller/src/commands/token.ts +88 -0
- package/acp-seller/src/commands/wallet.ts +123 -0
- package/acp-seller/src/lib/api.ts +118 -0
- package/acp-seller/src/lib/auth.ts +291 -0
- package/acp-seller/src/lib/bounty.ts +257 -0
- package/acp-seller/src/lib/client.ts +42 -0
- package/acp-seller/src/lib/config.ts +240 -0
- package/acp-seller/src/lib/open.ts +41 -0
- package/acp-seller/src/lib/openclawCron.ts +138 -0
- package/acp-seller/src/lib/output.ts +104 -0
- package/acp-seller/src/lib/wallet.ts +81 -0
- package/acp-seller/src/seller/offerings/_shared/preTransactionScan.ts +127 -0
- package/acp-seller/src/seller/offerings/canonical-catalog.ts +221 -0
- package/acp-seller/src/seller/offerings/spendos/spendos_summarize_url/handlers.ts +20 -0
- package/acp-seller/src/seller/offerings/spendos/spendos_summarize_url/offering.json +18 -0
- package/acp-seller/src/seller/offerings/spendos/spendos_translate/handlers.ts +21 -0
- package/acp-seller/src/seller/offerings/spendos/spendos_translate/offering.json +22 -0
- package/acp-seller/src/seller/offerings/spendos/spendos_tweet_gen/handlers.ts +20 -0
- package/acp-seller/src/seller/offerings/spendos/spendos_tweet_gen/offering.json +18 -0
- package/acp-seller/src/seller/runtime/acpSocket.ts +413 -0
- package/acp-seller/src/seller/runtime/logger.ts +36 -0
- package/acp-seller/src/seller/runtime/offeringTypes.ts +52 -0
- package/acp-seller/src/seller/runtime/offerings.ts +277 -0
- package/acp-seller/src/seller/runtime/paymentVerification.test.ts +207 -0
- package/acp-seller/src/seller/runtime/paymentVerification.ts +363 -0
- package/acp-seller/src/seller/runtime/seller.onchain.test.ts +220 -0
- package/acp-seller/src/seller/runtime/seller.test.ts +823 -0
- package/acp-seller/src/seller/runtime/seller.ts +1041 -0
- package/acp-seller/src/seller/runtime/sellerApi.ts +71 -0
- package/acp-seller/src/seller/runtime/startup.ts +270 -0
- package/acp-seller/src/seller/runtime/types.ts +62 -0
- package/acp-seller/tsconfig.json +20 -0
- package/bin/spendos.js +23 -0
- package/contracts/SpendOSAudit.sol +29 -0
- package/dist/mcp-server.mjs +153 -0
- package/jobs/translate.json +7 -0
- package/jobs/tweet-gen.json +7 -0
- package/openclaw.json +41 -0
- package/package.json +49 -0
- package/plugins/spendos-events/index.ts +78 -0
- package/plugins/spendos-events/package.json +14 -0
- package/policies/enforce-bounds.mjs +71 -0
- package/public/index.html +509 -0
- package/public/landing.html +241 -0
- package/railway.json +12 -0
- package/railway.toml +12 -0
- package/scripts/deploy.ts +48 -0
- package/scripts/test-x402-mainnet.ts +30 -0
- package/scripts/xmtp-listener.ts +61 -0
- package/setup.sh +278 -0
- package/skills/spendos/skill.md +26 -0
- package/src/agent.ts +152 -0
- package/src/audit.ts +166 -0
- package/src/governance.ts +367 -0
- package/src/job-registry.ts +306 -0
- package/src/mcp-public.ts +145 -0
- package/src/mcp-server.ts +171 -0
- package/src/opportunity-scanner.ts +138 -0
- package/src/server.ts +870 -0
- package/src/venice-x402.ts +234 -0
- package/src/xmtp.ts +109 -0
- package/src/zerion.ts +58 -0
- package/start.sh +168 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Seller API calls — accept/reject, request payment, deliver.
|
|
3
|
+
// =============================================================================
|
|
4
|
+
|
|
5
|
+
import client from "../../lib/client.js";
|
|
6
|
+
|
|
7
|
+
// -- Accept / Reject --
|
|
8
|
+
|
|
9
|
+
export interface AcceptOrRejectParams {
|
|
10
|
+
accept: boolean;
|
|
11
|
+
reason?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function acceptOrRejectJob(
|
|
15
|
+
jobId: number,
|
|
16
|
+
params: AcceptOrRejectParams,
|
|
17
|
+
): Promise<void> {
|
|
18
|
+
console.log(
|
|
19
|
+
`[sellerApi] acceptOrRejectJob jobId=${jobId} accept=${
|
|
20
|
+
params.accept
|
|
21
|
+
} reason=${params.reason ?? "(none)"}`,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
await client.post(`/acp/providers/jobs/${jobId}/accept`, params);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// -- Payment request --
|
|
28
|
+
|
|
29
|
+
export interface RequestPaymentParams {
|
|
30
|
+
content: string;
|
|
31
|
+
payableDetail?: {
|
|
32
|
+
amount: number;
|
|
33
|
+
tokenAddress: string;
|
|
34
|
+
recipient: string;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function requestPayment(
|
|
39
|
+
jobId: number,
|
|
40
|
+
params: RequestPaymentParams,
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
await client.post(`/acp/providers/jobs/${jobId}/requirement`, params);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// -- Deliver --
|
|
46
|
+
|
|
47
|
+
export interface DeliverJobParams {
|
|
48
|
+
deliverable: string | { type: string; value: unknown };
|
|
49
|
+
payableDetail?: {
|
|
50
|
+
amount: number;
|
|
51
|
+
tokenAddress: string;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function deliverJob(
|
|
56
|
+
jobId: number,
|
|
57
|
+
params: DeliverJobParams,
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
const delivStr =
|
|
60
|
+
typeof params.deliverable === "string"
|
|
61
|
+
? params.deliverable
|
|
62
|
+
: JSON.stringify(params.deliverable);
|
|
63
|
+
const transferStr = params.payableDetail
|
|
64
|
+
? ` transfer: ${params.payableDetail.amount} @ ${params.payableDetail.tokenAddress}`
|
|
65
|
+
: "";
|
|
66
|
+
console.log(
|
|
67
|
+
`[sellerApi] deliverJob jobId=${jobId} deliverable=${delivStr}${transferStr}`,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
return await client.post(`/acp/providers/jobs/${jobId}/deliverable`, params);
|
|
71
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Startup validation for seller runtime.
|
|
3
|
+
// Validates required environment variables and offering modules before starting.
|
|
4
|
+
// =============================================================================
|
|
5
|
+
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
import { listOfferings } from "./offerings.js";
|
|
10
|
+
import { createLogger } from "./logger.js";
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
|
|
15
|
+
const logger = createLogger("startup");
|
|
16
|
+
|
|
17
|
+
export interface ValidationResult {
|
|
18
|
+
valid: boolean;
|
|
19
|
+
errors: string[];
|
|
20
|
+
warnings: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface EnvVarCheck {
|
|
24
|
+
name: string;
|
|
25
|
+
required: boolean;
|
|
26
|
+
description: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Environment variables required or recommended for seller runtime.
|
|
31
|
+
*/
|
|
32
|
+
const ENV_VARS: EnvVarCheck[] = [
|
|
33
|
+
{
|
|
34
|
+
name: "LITE_AGENT_API_KEY",
|
|
35
|
+
required: true,
|
|
36
|
+
description: "API key for authenticating with ACP backend",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "ACP_SOCKET_URL",
|
|
40
|
+
required: false,
|
|
41
|
+
description: "WebSocket URL for ACP backend (defaults to production)",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "PAGERDUTY_ROUTING_KEY",
|
|
45
|
+
required: false,
|
|
46
|
+
description: "PagerDuty routing key for critical alerts",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "GUARDIAN_INTERNAL_API_TOKEN",
|
|
50
|
+
required: true,
|
|
51
|
+
description:
|
|
52
|
+
"Internal API token for Guardian scan offerings (HMAC signing)",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "NEXT_PUBLIC_SUPABASE_URL",
|
|
56
|
+
required: false,
|
|
57
|
+
description: "Supabase URL for database operations",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "SUPABASE_SERVICE_ROLE_KEY",
|
|
61
|
+
required: false,
|
|
62
|
+
description: "Supabase service role key for database operations",
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Validate environment variables are set correctly.
|
|
68
|
+
*/
|
|
69
|
+
function validateEnvironmentVariables(): {
|
|
70
|
+
errors: string[];
|
|
71
|
+
warnings: string[];
|
|
72
|
+
} {
|
|
73
|
+
const errors: string[] = [];
|
|
74
|
+
const warnings: string[] = [];
|
|
75
|
+
|
|
76
|
+
for (const envVar of ENV_VARS) {
|
|
77
|
+
const value = process.env[envVar.name];
|
|
78
|
+
|
|
79
|
+
if (!value || value.trim() === "") {
|
|
80
|
+
if (envVar.required) {
|
|
81
|
+
errors.push(
|
|
82
|
+
`Missing required environment variable: ${envVar.name} (${envVar.description})`,
|
|
83
|
+
);
|
|
84
|
+
} else {
|
|
85
|
+
warnings.push(
|
|
86
|
+
`Optional environment variable not set: ${envVar.name} (${envVar.description})`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
logger.debug(`Environment variable validated`, { envVar: envVar.name });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { errors, warnings };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Validate that offering modules exist and are loadable for the given agent.
|
|
99
|
+
*/
|
|
100
|
+
function validateOfferings(agentDirName: string): {
|
|
101
|
+
errors: string[];
|
|
102
|
+
warnings: string[];
|
|
103
|
+
} {
|
|
104
|
+
const errors: string[] = [];
|
|
105
|
+
const warnings: string[] = [];
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const offerings = listOfferings(agentDirName);
|
|
109
|
+
|
|
110
|
+
if (offerings.length === 0) {
|
|
111
|
+
warnings.push(
|
|
112
|
+
`No offerings found for agent "${agentDirName}". Seller will not accept any jobs.`,
|
|
113
|
+
);
|
|
114
|
+
return { errors, warnings };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
logger.info(`Validating ${offerings.length} offering(s)`, {
|
|
118
|
+
agentDirName,
|
|
119
|
+
offeringCount: offerings.length,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Validate each offering has required files
|
|
123
|
+
const offeringsRoot = path.resolve(
|
|
124
|
+
__dirname,
|
|
125
|
+
"..",
|
|
126
|
+
"offerings",
|
|
127
|
+
agentDirName,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
for (const offering of offerings) {
|
|
131
|
+
const offeringDir = path.join(offeringsRoot, offering);
|
|
132
|
+
const configPath = path.join(offeringDir, "offering.json");
|
|
133
|
+
const handlersPath = path.join(offeringDir, "handlers.ts");
|
|
134
|
+
|
|
135
|
+
if (!fs.existsSync(configPath)) {
|
|
136
|
+
errors.push(
|
|
137
|
+
`Offering "${offering}" missing offering.json at ${configPath}`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!fs.existsSync(handlersPath)) {
|
|
142
|
+
errors.push(
|
|
143
|
+
`Offering "${offering}" missing handlers.ts at ${handlersPath}`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Try to parse offering.json
|
|
148
|
+
if (fs.existsSync(configPath)) {
|
|
149
|
+
try {
|
|
150
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
151
|
+
const config = JSON.parse(content);
|
|
152
|
+
|
|
153
|
+
if (!config.name) {
|
|
154
|
+
errors.push(
|
|
155
|
+
`Offering "${offering}" offering.json missing required field: name`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
} catch (err) {
|
|
159
|
+
errors.push(
|
|
160
|
+
`Offering "${offering}" offering.json is not valid JSON: ${err}`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch (err) {
|
|
166
|
+
errors.push(
|
|
167
|
+
`Failed to validate offerings: ${err instanceof Error ? err.message : String(err)}`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { errors, warnings };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Validate Node.js version meets minimum requirements.
|
|
176
|
+
*/
|
|
177
|
+
function validateNodeVersion(): { errors: string[]; warnings: string[] } {
|
|
178
|
+
const errors: string[] = [];
|
|
179
|
+
const warnings: string[] = [];
|
|
180
|
+
|
|
181
|
+
const nodeVersion = process.version;
|
|
182
|
+
const majorVersion = parseInt(nodeVersion.slice(1).split(".")[0], 10);
|
|
183
|
+
|
|
184
|
+
logger.debug(`Node.js version detected`, { nodeVersion, majorVersion });
|
|
185
|
+
|
|
186
|
+
if (majorVersion < 18) {
|
|
187
|
+
errors.push(
|
|
188
|
+
`Node.js version ${nodeVersion} is too old. Minimum required: v18.0.0`,
|
|
189
|
+
);
|
|
190
|
+
} else if (majorVersion < 20) {
|
|
191
|
+
warnings.push(
|
|
192
|
+
`Node.js version ${nodeVersion} is supported but v20+ is recommended`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return { errors, warnings };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Run all startup validations.
|
|
201
|
+
* Returns a validation result with errors and warnings.
|
|
202
|
+
*/
|
|
203
|
+
export function validateStartup(agentDirName: string): ValidationResult {
|
|
204
|
+
logger.info("Running startup validation", { agentDirName });
|
|
205
|
+
|
|
206
|
+
const allErrors: string[] = [];
|
|
207
|
+
const allWarnings: string[] = [];
|
|
208
|
+
|
|
209
|
+
// 1. Validate Node version
|
|
210
|
+
const nodeCheck = validateNodeVersion();
|
|
211
|
+
allErrors.push(...nodeCheck.errors);
|
|
212
|
+
allWarnings.push(...nodeCheck.warnings);
|
|
213
|
+
|
|
214
|
+
// 2. Validate environment variables
|
|
215
|
+
const envCheck = validateEnvironmentVariables();
|
|
216
|
+
allErrors.push(...envCheck.errors);
|
|
217
|
+
allWarnings.push(...envCheck.warnings);
|
|
218
|
+
|
|
219
|
+
// 3. Validate offerings
|
|
220
|
+
const offeringCheck = validateOfferings(agentDirName);
|
|
221
|
+
allErrors.push(...offeringCheck.errors);
|
|
222
|
+
allWarnings.push(...offeringCheck.warnings);
|
|
223
|
+
|
|
224
|
+
const valid = allErrors.length === 0;
|
|
225
|
+
|
|
226
|
+
if (!valid) {
|
|
227
|
+
logger.error("Startup validation failed", {
|
|
228
|
+
errorCount: allErrors.length,
|
|
229
|
+
warningCount: allWarnings.length,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
for (const error of allErrors) {
|
|
233
|
+
logger.error(` - ${error}`);
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
logger.info("Startup validation passed", {
|
|
237
|
+
warningCount: allWarnings.length,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (allWarnings.length > 0) {
|
|
242
|
+
logger.warn(`Startup validation warnings (${allWarnings.length})`);
|
|
243
|
+
for (const warning of allWarnings) {
|
|
244
|
+
logger.warn(` - ${warning}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
valid,
|
|
250
|
+
errors: allErrors,
|
|
251
|
+
warnings: allWarnings,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Run startup validation and exit if it fails.
|
|
257
|
+
*/
|
|
258
|
+
export function validateStartupOrExit(agentDirName: string): void {
|
|
259
|
+
const result = validateStartup(agentDirName);
|
|
260
|
+
|
|
261
|
+
if (!result.valid) {
|
|
262
|
+
logger.error(
|
|
263
|
+
"Fatal: Startup validation failed. Cannot start seller runtime.",
|
|
264
|
+
{
|
|
265
|
+
errorCount: result.errors.length,
|
|
266
|
+
},
|
|
267
|
+
);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Minimal ACP types for the seller runtime.
|
|
3
|
+
// Standalone — no imports from @virtuals-protocol/acp-node.
|
|
4
|
+
// =============================================================================
|
|
5
|
+
|
|
6
|
+
/** Job lifecycle phases (mirrors AcpJobPhases from acp-node). */
|
|
7
|
+
export enum AcpJobPhase {
|
|
8
|
+
REQUEST = 0,
|
|
9
|
+
NEGOTIATION = 1,
|
|
10
|
+
TRANSACTION = 2,
|
|
11
|
+
EVALUATION = 3,
|
|
12
|
+
COMPLETED = 4,
|
|
13
|
+
REJECTED = 5,
|
|
14
|
+
EXPIRED = 6,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Memo types attached to a job (mirrors MemoType from acp-node). */
|
|
18
|
+
export enum MemoType {
|
|
19
|
+
MESSAGE = 0,
|
|
20
|
+
CONTEXT_URL = 1,
|
|
21
|
+
IMAGE_URL = 2,
|
|
22
|
+
VOICE_URL = 3,
|
|
23
|
+
OBJECT_URL = 4,
|
|
24
|
+
TXHASH = 5,
|
|
25
|
+
PAYABLE_REQUEST = 6,
|
|
26
|
+
PAYABLE_TRANSFER = 7,
|
|
27
|
+
PAYABLE_FEE = 8,
|
|
28
|
+
PAYABLE_FEE_REQUEST = 9,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Shape of a single memo as received from the ACP socket/API. */
|
|
32
|
+
export interface AcpMemoData {
|
|
33
|
+
id: number;
|
|
34
|
+
memoType: MemoType;
|
|
35
|
+
content: string;
|
|
36
|
+
nextPhase: AcpJobPhase;
|
|
37
|
+
expiry?: string | null;
|
|
38
|
+
createdAt?: string;
|
|
39
|
+
type?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Shape of the job payload delivered via socket `onNewTask` / `onEvaluate`. */
|
|
43
|
+
export interface AcpJobEventData {
|
|
44
|
+
id: number;
|
|
45
|
+
phase: AcpJobPhase;
|
|
46
|
+
clientAddress: string;
|
|
47
|
+
providerAddress: string;
|
|
48
|
+
evaluatorAddress: string;
|
|
49
|
+
price: number;
|
|
50
|
+
memos: AcpMemoData[];
|
|
51
|
+
context: Record<string, any>;
|
|
52
|
+
createdAt?: string;
|
|
53
|
+
/** The memo id the seller is expected to sign (if any). */
|
|
54
|
+
memoToSign?: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Socket event names used by the ACP backend. */
|
|
58
|
+
export enum SocketEvent {
|
|
59
|
+
ROOM_JOINED = "roomJoined",
|
|
60
|
+
ON_NEW_TASK = "onNewTask",
|
|
61
|
+
ON_EVALUATE = "onEvaluate",
|
|
62
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"declarationMap": true,
|
|
13
|
+
"sourceMap": true,
|
|
14
|
+
"outDir": "dist",
|
|
15
|
+
"rootDir": ".",
|
|
16
|
+
"lib": ["ES2022"]
|
|
17
|
+
},
|
|
18
|
+
"include": ["bin/**/*.ts", "src/**/*.ts"],
|
|
19
|
+
"exclude": ["node_modules", "dist", "scripts", "seller"]
|
|
20
|
+
}
|
package/bin/spendos.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execSync, spawn } from 'node:child_process';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { resolve, dirname } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const root = resolve(__dirname, '..');
|
|
10
|
+
|
|
11
|
+
// If setup.sh exists, run the interactive setup
|
|
12
|
+
const setupScript = resolve(root, 'setup.sh');
|
|
13
|
+
if (existsSync(setupScript)) {
|
|
14
|
+
const child = spawn('bash', [setupScript, ...process.argv.slice(2)], {
|
|
15
|
+
cwd: root,
|
|
16
|
+
stdio: 'inherit',
|
|
17
|
+
env: { ...process.env },
|
|
18
|
+
});
|
|
19
|
+
child.on('exit', (code) => process.exit(code ?? 0));
|
|
20
|
+
} else {
|
|
21
|
+
console.error('SpendOS setup.sh not found. Run from the spendos directory.');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.20;
|
|
3
|
+
|
|
4
|
+
contract SpendOSAudit {
|
|
5
|
+
event SpendEvent(
|
|
6
|
+
address indexed agent,
|
|
7
|
+
bytes32 indexed delegationHash,
|
|
8
|
+
string action,
|
|
9
|
+
string details,
|
|
10
|
+
uint256 amount,
|
|
11
|
+
uint256 timestamp
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
function log(
|
|
15
|
+
bytes32 delegationHash,
|
|
16
|
+
string calldata action,
|
|
17
|
+
string calldata details,
|
|
18
|
+
uint256 amount
|
|
19
|
+
) external {
|
|
20
|
+
emit SpendEvent(
|
|
21
|
+
msg.sender,
|
|
22
|
+
delegationHash,
|
|
23
|
+
action,
|
|
24
|
+
details,
|
|
25
|
+
amount,
|
|
26
|
+
block.timestamp
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/mcp-server.ts
|
|
4
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import {
|
|
7
|
+
CallToolRequestSchema,
|
|
8
|
+
ListToolsRequestSchema
|
|
9
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
+
var SPENDOS_URL = process.env.SPENDOS_URL ?? "http://localhost:3030";
|
|
11
|
+
var server = new Server(
|
|
12
|
+
{ name: "spendos", version: "0.1.0" },
|
|
13
|
+
{ capabilities: { tools: {} } }
|
|
14
|
+
);
|
|
15
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
16
|
+
tools: [
|
|
17
|
+
{
|
|
18
|
+
name: "summarize_url",
|
|
19
|
+
description: "Summarize a URL into a concise brief. Costs $0.01 via x402. Powered by Venice AI (wallet-authenticated, decentralized inference).",
|
|
20
|
+
inputSchema: {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
url: { type: "string", description: "The URL to summarize" }
|
|
24
|
+
},
|
|
25
|
+
required: ["url"]
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "generate_image",
|
|
30
|
+
description: "Generate an image from a text prompt. Costs $0.05 via x402. Powered by Venice AI.",
|
|
31
|
+
inputSchema: {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: {
|
|
34
|
+
prompt: { type: "string", description: "Text description of the image to generate" }
|
|
35
|
+
},
|
|
36
|
+
required: ["prompt"]
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "check_pnl",
|
|
41
|
+
description: "Check the SpendOS agent P&L (earnings, spending, profit margin).",
|
|
42
|
+
inputSchema: { type: "object", properties: {} }
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "request_delegation",
|
|
46
|
+
description: "Request a spending delegation from SpendOS governance. Must be approved by the wallet owner before the agent can sign transactions.",
|
|
47
|
+
inputSchema: {
|
|
48
|
+
type: "object",
|
|
49
|
+
properties: {
|
|
50
|
+
reason: { type: "string", description: "Why the agent needs spending permission" },
|
|
51
|
+
chains: { type: "array", items: { type: "string" }, description: "CAIP-2 chain IDs (e.g. eip155:8453)" },
|
|
52
|
+
totalBudget: { type: "string", description: "Maximum total spend in USD" },
|
|
53
|
+
expiresInMinutes: { type: "number", description: "How many minutes until the delegation expires" }
|
|
54
|
+
},
|
|
55
|
+
required: ["reason"]
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}));
|
|
60
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
61
|
+
const { name, arguments: args } = request.params;
|
|
62
|
+
switch (name) {
|
|
63
|
+
case "summarize_url": {
|
|
64
|
+
const res = await fetch(`${SPENDOS_URL}/api/summarize`, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: { "Content-Type": "application/json" },
|
|
67
|
+
body: JSON.stringify({ url: args?.url })
|
|
68
|
+
});
|
|
69
|
+
if (res.status === 402) {
|
|
70
|
+
return { content: [{ type: "text", text: "Payment required: $0.01 via x402. This endpoint is gated by the x402 payment protocol." }] };
|
|
71
|
+
}
|
|
72
|
+
const data = await res.json();
|
|
73
|
+
return {
|
|
74
|
+
content: [{
|
|
75
|
+
type: "text",
|
|
76
|
+
text: `Summary: ${data.summary}
|
|
77
|
+
|
|
78
|
+
Cost: earned $${data.cost?.earned}, inference $${data.cost?.inference}, profit $${data.cost?.profit}`
|
|
79
|
+
}]
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
case "generate_image": {
|
|
83
|
+
const res = await fetch(`${SPENDOS_URL}/api/generate-image`, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: { "Content-Type": "application/json" },
|
|
86
|
+
body: JSON.stringify({ prompt: args?.prompt })
|
|
87
|
+
});
|
|
88
|
+
if (res.status === 402) {
|
|
89
|
+
return { content: [{ type: "text", text: "Payment required: $0.05 via x402." }] };
|
|
90
|
+
}
|
|
91
|
+
const data = await res.json();
|
|
92
|
+
const imageUrl = data.images?.[0]?.url ?? data.images?.[0]?.b64_json ? "[base64 image]" : "no image";
|
|
93
|
+
return {
|
|
94
|
+
content: [{ type: "text", text: `Image generated: ${imageUrl}
|
|
95
|
+
Cost: earned $0.05, inference $0.01, profit $0.04` }]
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
case "check_pnl": {
|
|
99
|
+
const res = await fetch(`${SPENDOS_URL}/api/pnl`);
|
|
100
|
+
const pnl = await res.json();
|
|
101
|
+
const margin = pnl.totalEarned > 0 ? (pnl.profit / pnl.totalEarned * 100).toFixed(0) : "n/a";
|
|
102
|
+
return {
|
|
103
|
+
content: [{
|
|
104
|
+
type: "text",
|
|
105
|
+
text: `Agent P&L:
|
|
106
|
+
Earned: $${pnl.totalEarned.toFixed(3)}
|
|
107
|
+
Spent: $${pnl.totalSpent.toFixed(4)}
|
|
108
|
+
Profit: $${pnl.profit.toFixed(4)}
|
|
109
|
+
Queries: ${pnl.queryCount}
|
|
110
|
+
Margin: ${margin}%`
|
|
111
|
+
}]
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
case "request_delegation": {
|
|
115
|
+
const expiresAt = new Date(Date.now() + (Number(args?.expiresInMinutes) || 30) * 6e4).toISOString();
|
|
116
|
+
const res = await fetch(`${SPENDOS_URL}/api/delegate`, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers: { "Content-Type": "application/json" },
|
|
119
|
+
body: JSON.stringify({
|
|
120
|
+
agentAddress: "0x0000000000000000000000000000000000000000",
|
|
121
|
+
// MCP agent
|
|
122
|
+
reason: args?.reason ?? "MCP tool request",
|
|
123
|
+
chains: args?.chains ?? ["eip155:8453"],
|
|
124
|
+
operations: ["sign_tx"],
|
|
125
|
+
maxAmountPerAction: args?.totalBudget ?? "1.00",
|
|
126
|
+
totalBudget: args?.totalBudget ?? "1.00",
|
|
127
|
+
expiresAt
|
|
128
|
+
})
|
|
129
|
+
});
|
|
130
|
+
const d = await res.json();
|
|
131
|
+
return {
|
|
132
|
+
content: [{
|
|
133
|
+
type: "text",
|
|
134
|
+
text: `Delegation requested:
|
|
135
|
+
ID: ${d.id}
|
|
136
|
+
Status: ${d.status}
|
|
137
|
+
Risk: ${d.aiInterpretation?.riskLevel ?? "unknown"}
|
|
138
|
+
Warnings: ${d.aiInterpretation?.warnings?.join("; ") ?? "none"}
|
|
139
|
+
|
|
140
|
+
Awaiting wallet owner approval in SpendOS dashboard.`
|
|
141
|
+
}]
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
default:
|
|
145
|
+
return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
async function main() {
|
|
149
|
+
const transport = new StdioServerTransport();
|
|
150
|
+
await server.connect(transport);
|
|
151
|
+
console.error("[SpendOS MCP] Server started on stdio");
|
|
152
|
+
}
|
|
153
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tweet-gen",
|
|
3
|
+
"price": "$0.01",
|
|
4
|
+
"prompt": "Write a banger crypto twitter post about: {{topic}}. Make it punchy, authentic, no corporate speak. Use line breaks for readability. Include relevant hashtags. Max 280 chars per tweet but can be a thread of 2-3 tweets if needed.",
|
|
5
|
+
"inputs": ["topic"],
|
|
6
|
+
"description": "Generate viral crypto tweets from a topic"
|
|
7
|
+
}
|
package/openclaw.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "spendos-agent",
|
|
3
|
+
"description": "SpendOS autonomous agent with governed spending",
|
|
4
|
+
"mcpServers": {
|
|
5
|
+
"moonpay": {
|
|
6
|
+
"command": "npx",
|
|
7
|
+
"args": ["@moonpay/cli", "mcp"],
|
|
8
|
+
"toolFilter": {
|
|
9
|
+
"allow": [
|
|
10
|
+
"token_search",
|
|
11
|
+
"token_retrieve",
|
|
12
|
+
"token_quote",
|
|
13
|
+
"token_balance_list",
|
|
14
|
+
"token_check",
|
|
15
|
+
"token_trending_list",
|
|
16
|
+
"token_holder_list",
|
|
17
|
+
"chain_list",
|
|
18
|
+
"chain_retrieve",
|
|
19
|
+
"wallet_list",
|
|
20
|
+
"wallet_retrieve"
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"zerion": {
|
|
25
|
+
"url": "https://developers.zerion.io/mcp",
|
|
26
|
+
"transport": "streamable-http"
|
|
27
|
+
},
|
|
28
|
+
"spendos": {
|
|
29
|
+
"command": "npx",
|
|
30
|
+
"args": ["tsx", "src/mcp-server.ts"],
|
|
31
|
+
"env": {
|
|
32
|
+
"SPENDOS_URL": "http://localhost:3030"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"agents": {
|
|
37
|
+
"defaults": {
|
|
38
|
+
"workspace": "."
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|