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,234 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { privateKeyToAccount } from 'viem/accounts';
3
+
4
+ // ── Config ─────────────────────────────────────────────
5
+
6
+ const VENICE_API = 'https://api.venice.ai/api/v1';
7
+ const SIWE_DOMAIN = 'outerface.venice.ai';
8
+ const BASE_CHAIN_ID = 8453;
9
+ const VENICE_PAY_TO = '0x2670B922ef37C7Df47158725C0CC407b5382293F';
10
+ const USDC_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
11
+
12
+ let walletKey: `0x${string}` | null = null;
13
+ let walletAddress: string | null = null;
14
+
15
+ export function initVeniceWallet(privateKey: string, address: string): void {
16
+ walletKey = privateKey as `0x${string}`;
17
+ walletAddress = address;
18
+ console.log(`[Venice x402] Wallet configured: ${address}`);
19
+ }
20
+
21
+ // ── SIWE Header ────────────────────────────────────────
22
+
23
+ export async function buildSiweHeader(uri: string): Promise<string> {
24
+ if (!walletKey) throw new Error('Venice wallet not configured');
25
+
26
+ const account = privateKeyToAccount(walletKey);
27
+ const now = new Date();
28
+ const expiry = new Date(now.getTime() + 10 * 60 * 1000);
29
+ const nonce = randomBytes(16).toString('hex');
30
+
31
+ const message = [
32
+ `${SIWE_DOMAIN} wants you to sign in with your Ethereum account:`,
33
+ account.address,
34
+ '',
35
+ 'Sign in to Venice AI',
36
+ '',
37
+ `URI: ${uri}`,
38
+ `Version: 1`,
39
+ `Chain ID: ${BASE_CHAIN_ID}`,
40
+ `Nonce: ${nonce}`,
41
+ `Issued At: ${now.toISOString()}`,
42
+ `Expiration Time: ${expiry.toISOString()}`,
43
+ ].join('\n');
44
+
45
+ const signature = await account.signMessage({ message });
46
+
47
+ return Buffer.from(JSON.stringify({
48
+ address: account.address.toLowerCase(),
49
+ message,
50
+ signature,
51
+ chainId: BASE_CHAIN_ID,
52
+ timestamp: now.getTime(),
53
+ }), 'utf-8').toString('base64');
54
+ }
55
+
56
+ // ── Balance Check ──────────────────────────────────────
57
+
58
+ export interface VeniceBalance {
59
+ canConsume: boolean;
60
+ balanceUsd: number;
61
+ diemBalanceUsd: number;
62
+ }
63
+
64
+ export async function checkBalance(): Promise<VeniceBalance> {
65
+ if (!walletAddress) throw new Error('Venice wallet not configured');
66
+
67
+ const uri = `https://${SIWE_DOMAIN}/api/v1/x402/balance/${walletAddress}`;
68
+ const header = await buildSiweHeader(uri);
69
+
70
+ const res = await fetch(`${VENICE_API}/x402/balance/${walletAddress}`, {
71
+ headers: { 'X-Sign-In-With-X': header },
72
+ });
73
+
74
+ if (!res.ok) {
75
+ const text = await res.text();
76
+ throw new Error(`Venice balance check failed: ${res.status} ${text}`);
77
+ }
78
+
79
+ const data = await res.json() as any;
80
+ return {
81
+ canConsume: data.canConsume ?? false,
82
+ balanceUsd: parseFloat(data.balanceUsd ?? '0'),
83
+ diemBalanceUsd: parseFloat(data.diemBalanceUsd ?? '0'),
84
+ };
85
+ }
86
+
87
+ // ── Top-up via x402 ────────────────────────────────────
88
+
89
+ export async function topUp(amountUsd: number = 5): Promise<{ credited: number; newBalance: number } | null> {
90
+ if (!walletKey) throw new Error('Venice wallet not configured');
91
+
92
+ try {
93
+ const account = privateKeyToAccount(walletKey);
94
+ const amountBaseUnits = BigInt(Math.round(amountUsd * 1e6));
95
+ const now = Math.floor(Date.now() / 1000);
96
+ const validAfter = BigInt(now - 60);
97
+ const validBefore = BigInt(now + 900);
98
+ const nonce = `0x${Array.from(crypto.getRandomValues(new Uint8Array(32))).map(b => b.toString(16).padStart(2, '0')).join('')}`;
99
+
100
+ // EIP-3009 transferWithAuthorization signature
101
+ const signature = await account.signTypedData({
102
+ domain: { name: 'USD Coin', version: '2', chainId: BASE_CHAIN_ID, verifyingContract: USDC_BASE as `0x${string}` },
103
+ types: {
104
+ TransferWithAuthorization: [
105
+ { name: 'from', type: 'address' }, { name: 'to', type: 'address' },
106
+ { name: 'value', type: 'uint256' }, { name: 'validAfter', type: 'uint256' },
107
+ { name: 'validBefore', type: 'uint256' }, { name: 'nonce', type: 'bytes32' },
108
+ ],
109
+ },
110
+ primaryType: 'TransferWithAuthorization',
111
+ message: {
112
+ from: account.address, to: VENICE_PAY_TO as `0x${string}`,
113
+ value: amountBaseUnits, validAfter, validBefore, nonce: nonce as `0x${string}`,
114
+ },
115
+ });
116
+
117
+ // Build x402 payment header with 'accepted' field (Venice requires this)
118
+ const header = Buffer.from(JSON.stringify({
119
+ x402Version: 2,
120
+ scheme: 'exact',
121
+ network: `eip155:${BASE_CHAIN_ID}`,
122
+ accepted: {
123
+ scheme: 'exact',
124
+ network: `eip155:${BASE_CHAIN_ID}`,
125
+ payTo: VENICE_PAY_TO,
126
+ amount: amountBaseUnits.toString(),
127
+ asset: USDC_BASE,
128
+ extra: { name: 'USD Coin', version: '2' },
129
+ resource: `${VENICE_API}/x402/top-up`,
130
+ description: 'Venice x402 top-up',
131
+ mimeType: 'application/json',
132
+ maxTimeoutSeconds: 300,
133
+ },
134
+ payload: {
135
+ signature,
136
+ authorization: {
137
+ from: account.address, to: VENICE_PAY_TO,
138
+ value: amountBaseUnits.toString(),
139
+ validAfter: validAfter.toString(),
140
+ validBefore: validBefore.toString(),
141
+ nonce,
142
+ },
143
+ },
144
+ })).toString('base64');
145
+
146
+ const res = await fetch(`${VENICE_API}/x402/top-up`, {
147
+ method: 'POST',
148
+ headers: { 'X-402-Payment': header },
149
+ });
150
+
151
+ if (!res.ok) {
152
+ const text = await res.text();
153
+ throw new Error(`Top-up failed: ${res.status} ${text}`);
154
+ }
155
+
156
+ const data = await res.json() as any;
157
+ const credited = data.data?.amountCredited ?? amountUsd;
158
+ const newBal = data.data?.newBalance ?? 0;
159
+ console.log(`[Venice x402] Topped up $${credited} — new balance: $${newBal}`);
160
+ return { credited, newBalance: newBal };
161
+ } catch (err) {
162
+ console.error(`[Venice x402] Top-up failed: ${err}`);
163
+ return null;
164
+ }
165
+ }
166
+
167
+ // ── Inference via wallet auth ──────────────────────────
168
+
169
+ export async function walletInference(
170
+ model: string,
171
+ systemPrompt: string,
172
+ userContent: string,
173
+ maxTokens: number = 200,
174
+ ): Promise<string> {
175
+ const uri = `https://${SIWE_DOMAIN}/api/v1/chat/completions`;
176
+ const header = await buildSiweHeader(uri);
177
+
178
+ const res = await fetch(`${VENICE_API}/chat/completions`, {
179
+ method: 'POST',
180
+ headers: {
181
+ 'Content-Type': 'application/json',
182
+ 'X-Sign-In-With-X': header,
183
+ },
184
+ body: JSON.stringify({
185
+ model,
186
+ messages: [
187
+ { role: 'system', content: systemPrompt },
188
+ { role: 'user', content: userContent },
189
+ ],
190
+ max_tokens: maxTokens,
191
+ }),
192
+ });
193
+
194
+ if (res.status === 402) {
195
+ // Insufficient balance — try auto top-up
196
+ console.log('[Venice x402] Balance insufficient, attempting auto top-up...');
197
+ const result = await topUp(5);
198
+ if (!result) throw new Error('Auto top-up failed and Venice balance is insufficient');
199
+
200
+ // Retry inference after top-up
201
+ const retryHeader = await buildSiweHeader(uri);
202
+ const retryRes = await fetch(`${VENICE_API}/chat/completions`, {
203
+ method: 'POST',
204
+ headers: {
205
+ 'Content-Type': 'application/json',
206
+ 'X-Sign-In-With-X': retryHeader,
207
+ },
208
+ body: JSON.stringify({
209
+ model,
210
+ messages: [
211
+ { role: 'system', content: systemPrompt },
212
+ { role: 'user', content: userContent },
213
+ ],
214
+ max_tokens: maxTokens,
215
+ }),
216
+ });
217
+
218
+ if (!retryRes.ok) {
219
+ const text = await retryRes.text();
220
+ throw new Error(`Venice inference failed after top-up: ${retryRes.status} ${text}`);
221
+ }
222
+
223
+ const retryData = await retryRes.json() as any;
224
+ return retryData.choices?.[0]?.message?.content ?? 'No summary generated.';
225
+ }
226
+
227
+ if (!res.ok) {
228
+ const text = await res.text();
229
+ throw new Error(`Venice inference failed: ${res.status} ${text}`);
230
+ }
231
+
232
+ const data = await res.json() as any;
233
+ return data.choices?.[0]?.message?.content ?? 'No summary generated.';
234
+ }
package/src/xmtp.ts ADDED
@@ -0,0 +1,109 @@
1
+ import type { DelegationRequest } from './governance.js';
2
+
3
+ // ── State ──────────────────────────────────────────────
4
+
5
+ let xmtpReady = false;
6
+ let notifyAddress: string | null = null;
7
+ let xmtpClient: any = null;
8
+ let ownerAddress: string | null = null;
9
+
10
+ // ── Init ───────────────────────────────────────────────
11
+
12
+ export async function initXmtp(): Promise<string | null> {
13
+ try {
14
+ const { Client } = await import('@xmtp/node-sdk');
15
+ const { createWallet, getWallet, exportWallet } = await import('@open-wallet-standard/core');
16
+ const { mnemonicToAccount } = await import('viem/accounts');
17
+ const { toBytes } = await import('viem');
18
+ const { getRandomValues } = await import('node:crypto');
19
+
20
+ // Use a separate notify wallet — treasury keys never touch XMTP
21
+ const NOTIFY_WALLET = 'spendos-notify';
22
+ const OWS_VAULT = process.env.OWS_VAULT_PATH;
23
+ try { getWallet(NOTIFY_WALLET, OWS_VAULT); } catch { createWallet(NOTIFY_WALLET, undefined, undefined, OWS_VAULT); }
24
+
25
+ const mnemonic = exportWallet(NOTIFY_WALLET, undefined, OWS_VAULT);
26
+ const account = mnemonicToAccount(mnemonic);
27
+ notifyAddress = account.address;
28
+
29
+ // Get owner (treasury) address to send notifications TO
30
+ try {
31
+ const treasuryMnemonic = exportWallet('spendos-treasury', process.env.OWS_PASSPHRASE, OWS_VAULT);
32
+ const treasuryAccount = mnemonicToAccount(treasuryMnemonic);
33
+ ownerAddress = treasuryAccount.address;
34
+ } catch {
35
+ ownerAddress = null;
36
+ }
37
+
38
+ const signer = {
39
+ type: 'EOA' as const,
40
+ getIdentifier: () => ({ identifier: account.address, identifierKind: 0 }),
41
+ signMessage: async (message: string) => {
42
+ const sig = await account.signMessage({ message });
43
+ return toBytes(sig);
44
+ },
45
+ };
46
+
47
+ const dbEncryptionKey = getRandomValues(new Uint8Array(32));
48
+ xmtpClient = await Client.create(signer, { dbEncryptionKey });
49
+ xmtpReady = true;
50
+ console.log(`[XMTP] Client ready: ${notifyAddress} → notifications to ${ownerAddress ?? 'none'}`);
51
+ return notifyAddress;
52
+ } catch (err: any) {
53
+ console.log(`[XMTP] Init failed: ${err.message ?? err}`);
54
+ if (err.cause) console.log(`[XMTP] Cause: ${err.cause}`);
55
+ return null;
56
+ }
57
+ }
58
+
59
+ // ── Send XMTP Message ─────────────────────────────────
60
+
61
+ async function sendXmtpMessage(text: string): Promise<boolean> {
62
+ if (!xmtpReady || !xmtpClient || !ownerAddress) return false;
63
+
64
+ try {
65
+ // Create or get conversation with the owner wallet
66
+ const conversation = await xmtpClient.conversations.newDm(ownerAddress);
67
+ await conversation.send(text);
68
+ console.log(`[XMTP] Sent to ${ownerAddress}: ${text.slice(0, 80)}`);
69
+ return true;
70
+ } catch (err: any) {
71
+ console.log(`[XMTP] Send failed: ${err.message ?? err}`);
72
+ return false;
73
+ }
74
+ }
75
+
76
+ // ── Notifications ─────────────────────────────────────
77
+
78
+ export async function notifyDelegationRequest(d: DelegationRequest): Promise<boolean> {
79
+ const text = [
80
+ `🔔 SpendOS Delegation Request`,
81
+ ``,
82
+ `Agent: ${d.agentAddress}`,
83
+ `Reason: ${d.reason}`,
84
+ `Chains: ${d.chains.join(', ')}`,
85
+ `Budget: $${d.totalBudget}`,
86
+ `Risk: ${d.aiInterpretation?.riskLevel?.toUpperCase() ?? 'unknown'}`,
87
+ ``,
88
+ `Approve at https://spendos.xyz`,
89
+ ].join('\n');
90
+
91
+ const sent = await sendXmtpMessage(text);
92
+ if (!sent) console.log(`[XMTP/log] ${text.replace(/\n/g, ' | ')}`);
93
+ return sent;
94
+ }
95
+
96
+ export async function notifyDelegationDecision(
97
+ d: DelegationRequest,
98
+ action: 'approved' | 'rejected' | 'revoked' | 'expired',
99
+ ): Promise<boolean> {
100
+ const emoji = { approved: '✅', rejected: '❌', revoked: '🔄', expired: '⏰' }[action];
101
+ const text = `${emoji} Delegation ${action.toUpperCase()}: ${d.reason}`;
102
+ const sent = await sendXmtpMessage(text);
103
+ if (!sent) console.log(`[XMTP/log] ${text}`);
104
+ return sent;
105
+ }
106
+
107
+ export function getXmtpAddress(): string | null {
108
+ return notifyAddress;
109
+ }
package/src/zerion.ts ADDED
@@ -0,0 +1,58 @@
1
+ import type { WalletPortfolio } from './governance.js';
2
+
3
+ const ZERION_API_KEY = process.env.ZERION_API_KEY ?? '';
4
+ const ZERION_BASE = 'https://api.zerion.io/v1';
5
+
6
+ export async function getWalletPortfolio(address: string): Promise<WalletPortfolio | null> {
7
+ if (!address) return null;
8
+
9
+ // Zerion API uses HTTP Basic Auth with API key as username
10
+ const authHeader = ZERION_API_KEY
11
+ ? `Basic ${Buffer.from(`${ZERION_API_KEY}:`).toString('base64')}`
12
+ : '';
13
+
14
+ try {
15
+ const res = await fetch(`${ZERION_BASE}/wallets/${address}/portfolio?currency=usd`, {
16
+ headers: authHeader ? {
17
+ 'Authorization': authHeader,
18
+ 'Accept': 'application/json',
19
+ } : { 'Accept': 'application/json' },
20
+ signal: AbortSignal.timeout(5000),
21
+ });
22
+
23
+ if (!res.ok) {
24
+ console.log(`[Zerion] Portfolio fetch failed: ${res.status}`);
25
+ return null;
26
+ }
27
+
28
+ const data = await res.json() as any;
29
+ const totalValue = data.data?.attributes?.total?.positions ?? 0;
30
+
31
+ // Get token positions
32
+ const tokensRes = await fetch(`${ZERION_BASE}/wallets/${address}/positions?filter[chain_ids]=base&currency=usd`, {
33
+ headers: authHeader ? {
34
+ 'Authorization': authHeader,
35
+ 'Accept': 'application/json',
36
+ } : { 'Accept': 'application/json' },
37
+ signal: AbortSignal.timeout(5000),
38
+ });
39
+
40
+ const tokens: WalletPortfolio['tokens'] = [];
41
+ if (tokensRes.ok) {
42
+ const tokensData = await tokensRes.json() as any;
43
+ for (const pos of (tokensData.data ?? []).slice(0, 10)) {
44
+ const attrs = pos.attributes;
45
+ tokens.push({
46
+ symbol: attrs?.fungible_info?.symbol ?? '???',
47
+ balance: String(attrs?.quantity?.float ?? '0'),
48
+ valueUsd: attrs?.value ?? 0,
49
+ });
50
+ }
51
+ }
52
+
53
+ return { totalValueUsd: totalValue, tokens };
54
+ } catch (err) {
55
+ console.log(`[Zerion] Error: ${err}`);
56
+ return null;
57
+ }
58
+ }
package/start.sh ADDED
@@ -0,0 +1,168 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ # Railway sets PORT (usually 8080). Export before anything starts.
5
+ export PORT=${PORT:-8080}
6
+ export SPENDOS_INTERNAL_URL="http://localhost:${PORT}"
7
+
8
+ echo "[SpendOS] Starting on port $PORT..."
9
+
10
+ # Symlink memory + jobs to persistent volume so they survive redeploys
11
+ mkdir -p /data/memory
12
+ ln -sfn /data/memory /app/memory
13
+
14
+ # Persist agent-created jobs on volume
15
+ mkdir -p /data/jobs
16
+ # Copy seed jobs to volume (don't overwrite agent-created ones)
17
+ cp -n /app/jobs/*.json /data/jobs/ 2>/dev/null || true
18
+ # Symlink so runtime reads/writes to the volume
19
+ rm -rf /app/jobs
20
+ ln -sfn /data/jobs /app/jobs
21
+
22
+ # 1. Start SpendOS Express server
23
+ npx tsx src/server.ts &
24
+ SPENDOS_PID=$!
25
+ sleep 3
26
+
27
+ echo "[SpendOS] Express server started on :${PORT:-3030} (PID $SPENDOS_PID)"
28
+
29
+ # 2. Configure OpenClaw (only on first boot — check for marker file)
30
+ FIRST_BOOT_MARKER="$OPENCLAW_STATE_DIR/.spendos-configured"
31
+ if [ ! -f "$FIRST_BOOT_MARKER" ]; then
32
+ echo "[SpendOS] First boot — configuring OpenClaw..."
33
+
34
+ # Set Venice model via SpendOS proxy
35
+ openclaw config set agents.defaults.model.primary "spendos/kimi-k2-5" 2>/dev/null || true
36
+ openclaw config set models.providers.spendos '{"baseUrl":"http://localhost:3030/v1","apiKey":"spendos","api":"openai-completions","models":[{"id":"kimi-k2-5","name":"kimi-k2-5","contextWindow":256000}]}' 2>/dev/null || true
37
+ openclaw config set agents.defaults.models '{"spendos/kimi-k2-5":{"alias":"Venice via SpendOS"}}' 2>/dev/null || true
38
+ openclaw config set gateway.mode local 2>/dev/null || true
39
+ openclaw config set gateway.http.endpoints.chatCompletions.enabled true 2>/dev/null || true
40
+
41
+ # Set SpendOS MCP server (pre-compiled JS — no tsx needed)
42
+ openclaw mcp set spendos "{\"command\":\"node\",\"args\":[\"/app/dist/mcp-server.mjs\"],\"env\":{\"SPENDOS_URL\":\"http://localhost:${PORT:-3030}\"}}" 2>/dev/null || true
43
+
44
+ # Set MoonPay MCP (safe tools only)
45
+ openclaw mcp set moonpay '{"command":"npx","args":["@moonpay/cli","mcp"]}' 2>/dev/null || true
46
+
47
+ # Set Zerion MCP
48
+ openclaw mcp set zerion '{"url":"https://developers.zerion.io/mcp","transport":"streamable-http"}' 2>/dev/null || true
49
+
50
+ # Install SpendOS skill + plugin into OpenClaw
51
+ # Install skill
52
+ mkdir -p $OPENCLAW_STATE_DIR/skills/spendos
53
+ cp /app/skills/spendos/skill.md $OPENCLAW_STATE_DIR/skills/spendos/ 2>/dev/null || true
54
+
55
+ # Install plugin
56
+ mkdir -p $OPENCLAW_STATE_DIR/plugins
57
+ cp -r /app/plugins/spendos-events $OPENCLAW_STATE_DIR/plugins/ 2>/dev/null || true
58
+ cd $OPENCLAW_STATE_DIR/plugins/spendos-events && npm install --production 2>/dev/null || true
59
+ cd /app
60
+
61
+ # Register plugin with OpenClaw
62
+ openclaw config set plugins.spendos-events "{\"path\":\"$OPENCLAW_STATE_DIR/plugins/spendos-events\"}" 2>/dev/null || true
63
+
64
+ # Set workspace to /app so SOUL.md and AGENTS.md are loaded
65
+ openclaw config set agents.defaults.workspace /app 2>/dev/null || true
66
+
67
+ # Security: block agent from reading secrets
68
+ openclaw config set tools.exec.env.denylist '["OWS_PASSPHRASE","OWS_IMPORT_MNEMONIC","DEPLOYER_PRIVATE_KEY","SPENDOS_ADMIN_TOKEN","OPENCLAW_GATEWAY_TOKEN_OVERRIDE","TWITTER_USERNAME","TWITTER_PASSWORD"]' 2>/dev/null || true
69
+
70
+ # Enable browser tool (Chromium installed in Docker image)
71
+ openclaw config set tools.browser.enabled true 2>/dev/null || true
72
+ openclaw config set tools.browser.executable "$CHROME_BIN" 2>/dev/null || true
73
+
74
+ # Restore Twitter cookies from persistent volume if they exist
75
+ if [ -f "/data/twitter-cookies.json" ]; then
76
+ openclaw config set tools.browser.cookies "/data/twitter-cookies.json" 2>/dev/null || true
77
+ echo "[SpendOS] Twitter cookies restored from volume"
78
+ fi
79
+
80
+ # Disable Slack channel (not needed, causes MODULE_NOT_FOUND errors)
81
+ openclaw config set channels.slack.enabled false 2>/dev/null || true
82
+
83
+ touch "$FIRST_BOOT_MARKER"
84
+ echo "[SpendOS] First boot configuration complete"
85
+ else
86
+ echo "[SpendOS] OpenClaw already configured (restarting with existing sessions/memory)"
87
+ fi
88
+
89
+ # ALWAYS update these on every boot (not just first boot)
90
+ if [ -n "$OPENCLAW_GATEWAY_TOKEN_OVERRIDE" ]; then
91
+ openclaw config set gateway.auth.token "$OPENCLAW_GATEWAY_TOKEN_OVERRIDE" 2>/dev/null || true
92
+ fi
93
+ # Re-register SpendOS MCP server (compiled JS, no tsx dependency)
94
+ openclaw mcp set spendos "{\"command\":\"node\",\"args\":[\"/app/dist/mcp-server.mjs\"],\"env\":{\"SPENDOS_URL\":\"http://localhost:${PORT:-3030}\"}}" 2>/dev/null || true
95
+
96
+ # 3. Set up cron jobs (if not already set)
97
+ # Get OpenClaw gateway token for internal API calls
98
+ # openclaw config get redacts the token, so read directly from the JSON config
99
+ export OPENCLAW_GATEWAY_TOKEN=$(node -e "try{const c=JSON.parse(require('fs').readFileSync('$OPENCLAW_STATE_DIR/openclaw.json','utf8'));process.stdout.write(c.gateway?.auth?.token||'')}catch{}" 2>/dev/null || echo "")
100
+ export OPENCLAW_INTERNAL_URL="http://localhost:18789"
101
+
102
+ # OpenClaw gateway on INTERNAL port only — NOT exposed to internet
103
+ openclaw gateway --port 18789 &
104
+ GATEWAY_PID=$!
105
+ sleep 5
106
+
107
+ echo "[SpendOS] OpenClaw gateway started on :18789 INTERNAL (PID $GATEWAY_PID)"
108
+
109
+ # Add cron jobs only on first boot
110
+ # COST AWARENESS: each cron invocation costs ~$0.05 in Venice inference.
111
+ # Only invoke the agent when there's revenue to justify it.
112
+ # The venice-topup cron is removed — auto top-up is now built into the proxy.
113
+ # Reset crons on every boot to pick up config changes
114
+ openclaw cron remove --name "revenue-scanner" 2>/dev/null || true
115
+ openclaw cron remove --name "venice-topup" 2>/dev/null || true
116
+ if true; then
117
+ openclaw cron add \
118
+ --name "revenue-scanner" \
119
+ --every "2h" \
120
+ --agent main \
121
+ --session isolated \
122
+ --timeout-seconds 90 \
123
+ --light-context \
124
+ --message "You are SpendOS. COST RULE: This cron costs ~\$0.05 per run. Only take actions that generate more than \$0.05 in expected value.
125
+
126
+ Quick check: curl -s http://localhost:3030/api/pnl
127
+
128
+ If earned > 0 and margin is improving, propose ONE yield delegation.
129
+ If no revenue, create ONE new job (write JSON to /app/jobs/ then curl -X POST http://localhost:3030/api/jobs/reload).
130
+ If you already created jobs recently, just report status — don't burn credits on busywork.
131
+ Keep responses SHORT. Every token costs money." \
132
+ --description "Cost-aware revenue scanner (2h)" \
133
+ 2>/dev/null || echo "[SpendOS] Cron already exists"
134
+
135
+ touch "$FIRST_BOOT_MARKER.crons"
136
+ echo "[SpendOS] Cron jobs configured"
137
+ else
138
+ echo "[SpendOS] Cron jobs already exist"
139
+ fi
140
+ # 4. Start ACP seller runtime (agent-to-agent commerce marketplace)
141
+ # Decode ACP config from base64 env var (injected by setup.sh --deploy)
142
+ if [ -n "$ACP_CONFIG_B64" ] && [ -d "/app/acp-seller" ]; then
143
+ echo "$ACP_CONFIG_B64" | base64 -d > /app/acp-seller/config.json
144
+ LITE_AGENT_API_KEY=$(node -e "try{const c=JSON.parse(require('fs').readFileSync('/app/acp-seller/config.json'));process.stdout.write(c.LITE_AGENT_API_KEY||'')}catch{}" 2>/dev/null)
145
+ export LITE_AGENT_API_KEY
146
+ echo "[SpendOS] ACP config decoded from ACP_CONFIG_B64"
147
+ fi
148
+
149
+ if [ -n "$LITE_AGENT_API_KEY" ] && [ -d "/app/acp-seller" ]; then
150
+ echo "[SpendOS] Starting ACP seller runtime..."
151
+ cd /app/acp-seller
152
+ SPENDOS_URL="http://localhost:${PORT:-8080}" GUARDIAN_INTERNAL_API_TOKEN="${GUARDIAN_INTERNAL_API_TOKEN:-skip}" \
153
+ npx tsx bin/acp.ts serve start 2>&1 || echo "[SpendOS] ACP seller failed to start"
154
+ cd /app
155
+ echo "[SpendOS] ACP seller runtime started (accepting jobs from other agents)"
156
+ else
157
+ echo "[SpendOS] ACP seller skipped (set LITE_AGENT_API_KEY to enable)"
158
+ fi
159
+
160
+ echo "[SpendOS] System ready:"
161
+ echo " Dashboard (PUBLIC): http://localhost:${PORT:-8080}"
162
+ echo " OpenClaw (INTERNAL): http://localhost:18789 (not exposed to internet)"
163
+ if [ -n "$LITE_AGENT_API_KEY" ]; then
164
+ echo " ACP Seller: connected to acpx.virtuals.io (3 offerings)"
165
+ fi
166
+
167
+ # Wait for either process to exit
168
+ wait $SPENDOS_PID $GATEWAY_PID
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "esModuleInterop": true,
7
+ "strict": true,
8
+ "outDir": "dist",
9
+ "rootDir": "src",
10
+ "skipLibCheck": true,
11
+ "resolveJsonModule": true
12
+ },
13
+ "include": ["src"]
14
+ }