bloby-bot 0.29.0 → 0.30.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/package.json +1 -1
- package/scripts/tempo-balance.ts +10 -10
- package/worker/codex-auth.ts +211 -47
- package/worker/index.ts +7 -7
- package/worker/prompts/bloby-system-prompt.txt +10 -0
- package/workspace/client/src/App.tsx +2 -2
package/package.json
CHANGED
package/scripts/tempo-balance.ts
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
// Read on-chain USDC balances for one or more Tempo addresses.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// cp scripts/tempo-balance.ts /tmp/tempo-transfer/ && \
|
|
5
|
-
// (cd /tmp/tempo-transfer && ./node_modules/.bin/tsx tempo-balance.ts)
|
|
6
|
-
//
|
|
7
|
-
// (The script must be executed from inside the isolated /tmp/tempo-transfer/
|
|
8
|
-
// install — the repo's own node_modules has a @noble/hashes version conflict
|
|
9
|
-
// that breaks viem's ESM import resolution. To rebuild the isolated dir if
|
|
10
|
-
// missing: mkdir -p /tmp/tempo-transfer && cd /tmp/tempo-transfer \
|
|
11
|
-
// && npm init -y && npm install viem@2.47.6 tsx)
|
|
3
|
+
// RUN (one-liner, from repo root): cp scripts/tempo-balance.ts /tmp/tempo-transfer/ && (cd /tmp/tempo-transfer && ./node_modules/.bin/tsx tempo-balance.ts)
|
|
12
4
|
//
|
|
5
|
+
// Edit the ACCOUNTS list in the CONFIG block below to add/remove wallets.
|
|
13
6
|
// Each entry prints: USDC balance, nonce (0 = wallet has never sent a tx),
|
|
14
7
|
// and whether the address is an EOA or a contract.
|
|
8
|
+
//
|
|
9
|
+
// Why the copy-into-/tmp dance: the repo's own node_modules has a
|
|
10
|
+
// @noble/hashes version conflict that breaks viem's ESM import resolution,
|
|
11
|
+
// so the script must run from the isolated /tmp/tempo-transfer/ install.
|
|
12
|
+
// To rebuild that dir if it's missing:
|
|
13
|
+
// mkdir -p /tmp/tempo-transfer && cd /tmp/tempo-transfer && npm init -y && npm install viem@2.47.6 tsx
|
|
15
14
|
|
|
16
15
|
// ─── CONFIG ──────────────────────────────────────────────────────────────────
|
|
17
16
|
|
|
18
17
|
const ACCOUNTS = [
|
|
19
|
-
{ label: '
|
|
18
|
+
{ label: 'Bloby', wallet: '0xc65d8a0dB80f637337B87e42b8BEb5D86D46f942' },
|
|
20
19
|
{ label: 'Treasury', wallet: '0x084A80e395FaE8Aaf58F591f47F63DA1EeF06bbC' },
|
|
20
|
+
{ label: 'Serge', wallet: '0x21AC79F74da90c9F441699Ee6215dA32c0CE9F5a' },
|
|
21
21
|
];
|
|
22
22
|
|
|
23
23
|
// ─────────────────────────────────────────────────────────────────────────────
|
package/worker/codex-auth.ts
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Codex OAuth PKCE flow for ChatGPT Plus/Pro subscription authentication.
|
|
3
|
-
* Adapted from CodeDeck's CodexOAuthService for server-side Node.js.
|
|
4
3
|
*
|
|
5
4
|
* Spins up a temporary HTTP server on port 1455 to capture the OAuth callback.
|
|
6
|
-
* Credentials are stored in ~/.codex/
|
|
5
|
+
* Credentials are stored in ~/.codex/auth.json in the same shape Codex CLI
|
|
6
|
+
* itself writes, so a spawned `codex app-server` process can use them directly.
|
|
7
|
+
*
|
|
8
|
+
* Shape on disk (chatgpt mode):
|
|
9
|
+
* {
|
|
10
|
+
* "OPENAI_API_KEY": null,
|
|
11
|
+
* "auth_mode": "chatgpt",
|
|
12
|
+
* "tokens": {
|
|
13
|
+
* "id_token": "<raw jwt>",
|
|
14
|
+
* "access_token": "<jwt>",
|
|
15
|
+
* "refresh_token": "...",
|
|
16
|
+
* "account_id": "<chatgpt_account_id from id_token claims>"
|
|
17
|
+
* },
|
|
18
|
+
* "last_refresh": "2026-05-03T12:34:56.789Z"
|
|
19
|
+
* }
|
|
7
20
|
*/
|
|
8
21
|
|
|
9
22
|
import crypto from 'crypto';
|
|
@@ -23,50 +36,124 @@ const OAUTH_CONFIG = {
|
|
|
23
36
|
};
|
|
24
37
|
|
|
25
38
|
const AUTH_DIR = path.join(os.homedir(), '.codex');
|
|
26
|
-
const AUTH_FILE = path.join(AUTH_DIR, '
|
|
39
|
+
const AUTH_FILE = path.join(AUTH_DIR, 'auth.json');
|
|
40
|
+
const LEGACY_AUTH_FILE = path.join(AUTH_DIR, 'codedeck-auth.json');
|
|
41
|
+
|
|
42
|
+
/** Refresh access tokens this many ms before they actually expire. */
|
|
43
|
+
const REFRESH_LEEWAY_MS = 5 * 60 * 1000;
|
|
27
44
|
|
|
28
45
|
let codeVerifier: string | null = null;
|
|
29
46
|
let oauthState: string | null = null;
|
|
30
47
|
let callbackServer: http.Server | null = null;
|
|
31
48
|
|
|
32
|
-
|
|
49
|
+
interface AuthDotJson {
|
|
50
|
+
OPENAI_API_KEY: string | null;
|
|
51
|
+
auth_mode?: 'apikey' | 'chatgpt' | 'chatgptAuthTokens' | 'agentIdentity';
|
|
52
|
+
tokens?: {
|
|
53
|
+
id_token: string;
|
|
54
|
+
access_token: string;
|
|
55
|
+
refresh_token: string;
|
|
56
|
+
account_id?: string | null;
|
|
57
|
+
};
|
|
58
|
+
last_refresh?: string;
|
|
59
|
+
[key: string]: unknown;
|
|
60
|
+
}
|
|
33
61
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
62
|
+
/* ── File I/O ── */
|
|
63
|
+
|
|
64
|
+
function readAuthFile(): AuthDotJson | null {
|
|
65
|
+
try {
|
|
66
|
+
if (!fs.existsSync(AUTH_FILE)) return null;
|
|
67
|
+
return JSON.parse(fs.readFileSync(AUTH_FILE, 'utf-8'));
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
38
70
|
}
|
|
39
71
|
}
|
|
40
72
|
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
73
|
+
function writeAuthFile(auth: AuthDotJson): void {
|
|
74
|
+
fs.mkdirSync(AUTH_DIR, { recursive: true });
|
|
75
|
+
fs.writeFileSync(AUTH_FILE, JSON.stringify(auth, null, 2), 'utf-8');
|
|
76
|
+
try { fs.chmodSync(AUTH_FILE, 0o600); } catch {}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Decode a JWT and return its parsed payload, or null on failure. */
|
|
80
|
+
function decodeJwt(token: string): Record<string, any> | null {
|
|
81
|
+
try {
|
|
82
|
+
const parts = token.split('.');
|
|
83
|
+
if (parts.length < 2) return null;
|
|
84
|
+
return JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
44
87
|
}
|
|
88
|
+
}
|
|
45
89
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
90
|
+
/** Read the JWT `exp` claim as a Unix epoch (seconds). Null if missing/invalid. */
|
|
91
|
+
function jwtExpiryMs(token: string): number | null {
|
|
92
|
+
const payload = decodeJwt(token);
|
|
93
|
+
if (!payload || typeof payload.exp !== 'number') return null;
|
|
94
|
+
return payload.exp * 1000;
|
|
95
|
+
}
|
|
50
96
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
97
|
+
/** Pull `chatgpt_account_id` out of the id_token JWT claims, if present. */
|
|
98
|
+
function extractAccountId(idToken: string): string | undefined {
|
|
99
|
+
const payload = decodeJwt(idToken);
|
|
100
|
+
const authClaims = payload?.['https://api.openai.com/auth'] || {};
|
|
101
|
+
return authClaims.chatgpt_account_id || undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** One-shot migration from the old `codedeck-auth.json` layout (pre-codex-native). */
|
|
105
|
+
function migrateLegacyFile(): void {
|
|
106
|
+
if (fs.existsSync(AUTH_FILE)) {
|
|
107
|
+
// If auth.json already has chatgpt tokens, nothing to do.
|
|
108
|
+
const existing = readAuthFile();
|
|
109
|
+
if (existing?.tokens?.refresh_token) return;
|
|
55
110
|
}
|
|
111
|
+
if (!fs.existsSync(LEGACY_AUTH_FILE)) return;
|
|
56
112
|
|
|
57
|
-
// Decode JWT claims for account info
|
|
58
113
|
try {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
114
|
+
const legacy = JSON.parse(fs.readFileSync(LEGACY_AUTH_FILE, 'utf-8'));
|
|
115
|
+
if (!legacy.access_token || !legacy.refresh_token) return;
|
|
116
|
+
|
|
117
|
+
const existing = readAuthFile() || { OPENAI_API_KEY: null };
|
|
118
|
+
const next: AuthDotJson = {
|
|
119
|
+
...existing,
|
|
120
|
+
auth_mode: 'chatgpt',
|
|
121
|
+
tokens: {
|
|
122
|
+
id_token: legacy.id_token || '',
|
|
123
|
+
access_token: legacy.access_token,
|
|
124
|
+
refresh_token: legacy.refresh_token,
|
|
125
|
+
account_id: legacy.chatgpt_account_id || extractAccountId(legacy.access_token),
|
|
126
|
+
},
|
|
127
|
+
last_refresh: new Date().toISOString(),
|
|
128
|
+
};
|
|
129
|
+
writeAuthFile(next);
|
|
130
|
+
try { fs.unlinkSync(LEGACY_AUTH_FILE); } catch {}
|
|
131
|
+
log.ok('Codex: migrated legacy codedeck-auth.json → auth.json');
|
|
132
|
+
} catch (err: any) {
|
|
133
|
+
log.warn(`Codex: legacy migration failed — ${err.message}`);
|
|
65
134
|
}
|
|
135
|
+
}
|
|
66
136
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
137
|
+
/* ── Token exchange & refresh ── */
|
|
138
|
+
|
|
139
|
+
function storeTokens(tokens: { access_token: string; refresh_token?: string; id_token?: string }): void {
|
|
140
|
+
const existing = readAuthFile() || { OPENAI_API_KEY: null };
|
|
141
|
+
const prev = existing.tokens;
|
|
142
|
+
const idToken = tokens.id_token ?? prev?.id_token ?? '';
|
|
143
|
+
|
|
144
|
+
const next: AuthDotJson = {
|
|
145
|
+
...existing,
|
|
146
|
+
OPENAI_API_KEY: existing.OPENAI_API_KEY ?? null,
|
|
147
|
+
auth_mode: 'chatgpt',
|
|
148
|
+
tokens: {
|
|
149
|
+
id_token: idToken,
|
|
150
|
+
access_token: tokens.access_token,
|
|
151
|
+
refresh_token: tokens.refresh_token ?? prev?.refresh_token ?? '',
|
|
152
|
+
account_id: idToken ? extractAccountId(idToken) ?? prev?.account_id : prev?.account_id,
|
|
153
|
+
},
|
|
154
|
+
last_refresh: new Date().toISOString(),
|
|
155
|
+
};
|
|
156
|
+
writeAuthFile(next);
|
|
70
157
|
}
|
|
71
158
|
|
|
72
159
|
async function exchangeCode(code: string): Promise<{ success: boolean; error?: string }> {
|
|
@@ -94,27 +181,61 @@ async function exchangeCode(code: string): Promise<{ success: boolean; error?: s
|
|
|
94
181
|
}
|
|
95
182
|
|
|
96
183
|
const tokens = await response.json();
|
|
97
|
-
|
|
184
|
+
if (!tokens.access_token) {
|
|
185
|
+
return { success: false, error: 'OAuth response missing access_token.' };
|
|
186
|
+
}
|
|
187
|
+
storeTokens(tokens);
|
|
98
188
|
codeVerifier = null;
|
|
99
189
|
oauthState = null;
|
|
190
|
+
log.ok('Codex credentials stored');
|
|
191
|
+
// Clean up the legacy file on first successful new auth.
|
|
192
|
+
try { fs.unlinkSync(LEGACY_AUTH_FILE); } catch {}
|
|
100
193
|
return { success: true };
|
|
101
194
|
} catch (err: any) {
|
|
102
195
|
return { success: false, error: err.message };
|
|
103
196
|
}
|
|
104
197
|
}
|
|
105
198
|
|
|
199
|
+
async function refreshTokens(refreshToken: string): Promise<boolean> {
|
|
200
|
+
try {
|
|
201
|
+
log.ok('Codex: refreshing access token...');
|
|
202
|
+
const response = await fetch(OAUTH_CONFIG.TOKEN_URL, {
|
|
203
|
+
method: 'POST',
|
|
204
|
+
headers: { 'Content-Type': 'application/json' },
|
|
205
|
+
body: JSON.stringify({
|
|
206
|
+
client_id: OAUTH_CONFIG.CLIENT_ID,
|
|
207
|
+
grant_type: 'refresh_token',
|
|
208
|
+
refresh_token: refreshToken,
|
|
209
|
+
}),
|
|
210
|
+
});
|
|
211
|
+
if (!response.ok) {
|
|
212
|
+
log.warn(`Codex: refresh failed (${response.status})`);
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
const tokens = await response.json();
|
|
216
|
+
if (!tokens.access_token) {
|
|
217
|
+
log.warn('Codex: refresh response missing access_token');
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
storeTokens(tokens);
|
|
221
|
+
log.ok('Codex: token refreshed');
|
|
222
|
+
return true;
|
|
223
|
+
} catch (err: any) {
|
|
224
|
+
log.warn(`Codex: refresh error — ${err.message}`);
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
106
229
|
/* ── Public API ── */
|
|
107
230
|
|
|
108
231
|
export function startCodexOAuth(): Promise<{ success: boolean; authUrl?: string; error?: string }> {
|
|
109
232
|
return new Promise((resolve) => {
|
|
110
233
|
stopCallbackServer();
|
|
111
234
|
|
|
112
|
-
// Generate PKCE
|
|
113
235
|
codeVerifier = crypto.randomBytes(32).toString('base64url');
|
|
114
236
|
const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
|
|
115
237
|
oauthState = crypto.randomUUID();
|
|
116
238
|
|
|
117
|
-
// Start local callback server
|
|
118
239
|
callbackServer = http.createServer((req, res) => {
|
|
119
240
|
if (!req.url?.startsWith('/auth/callback')) {
|
|
120
241
|
res.writeHead(404);
|
|
@@ -172,28 +293,71 @@ export function cancelCodexOAuth(): void {
|
|
|
172
293
|
oauthState = null;
|
|
173
294
|
}
|
|
174
295
|
|
|
175
|
-
export function getCodexAuthStatus(): {
|
|
296
|
+
export async function getCodexAuthStatus(): Promise<{
|
|
297
|
+
authenticated: boolean;
|
|
298
|
+
plan?: string;
|
|
299
|
+
error?: string;
|
|
300
|
+
}> {
|
|
176
301
|
try {
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
if (
|
|
181
|
-
|
|
302
|
+
migrateLegacyFile();
|
|
303
|
+
const auth = readAuthFile();
|
|
304
|
+
const tokens = auth?.tokens;
|
|
305
|
+
if (!tokens?.access_token) return { authenticated: false };
|
|
306
|
+
|
|
307
|
+
const expMs = jwtExpiryMs(tokens.access_token);
|
|
308
|
+
const valid = expMs ? Date.now() + REFRESH_LEEWAY_MS < expMs : true;
|
|
309
|
+
|
|
310
|
+
if (!valid && tokens.refresh_token) {
|
|
311
|
+
const ok = await refreshTokens(tokens.refresh_token);
|
|
312
|
+
if (!ok) return { authenticated: false, error: 'Token expired and refresh failed' };
|
|
182
313
|
}
|
|
183
|
-
|
|
314
|
+
|
|
315
|
+
const fresh = readAuthFile();
|
|
316
|
+
const idClaims = fresh?.tokens?.id_token ? decodeJwt(fresh.tokens.id_token) : null;
|
|
317
|
+
const plan = idClaims?.['https://api.openai.com/auth']?.chatgpt_plan_type || 'plus';
|
|
318
|
+
return { authenticated: true, plan };
|
|
184
319
|
} catch (err: any) {
|
|
185
320
|
return { authenticated: false, error: err.message };
|
|
186
321
|
}
|
|
187
322
|
}
|
|
188
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Read a valid Codex access token, refreshing if expired. Returns null if unavailable.
|
|
326
|
+
* This is what the Codex harness should call before each app-server invocation.
|
|
327
|
+
*/
|
|
328
|
+
export async function getCodexAccessToken(): Promise<string | null> {
|
|
329
|
+
migrateLegacyFile();
|
|
330
|
+
const auth = readAuthFile();
|
|
331
|
+
const tokens = auth?.tokens;
|
|
332
|
+
if (!tokens?.access_token) return null;
|
|
333
|
+
|
|
334
|
+
const expMs = jwtExpiryMs(tokens.access_token);
|
|
335
|
+
if (!expMs || Date.now() + REFRESH_LEEWAY_MS < expMs) {
|
|
336
|
+
return tokens.access_token;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (!tokens.refresh_token) return null;
|
|
340
|
+
const ok = await refreshTokens(tokens.refresh_token);
|
|
341
|
+
if (!ok) return null;
|
|
342
|
+
|
|
343
|
+
return readAuthFile()?.tokens?.access_token ?? null;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Synchronous accessor — returns the stored access token without refreshing. */
|
|
189
347
|
export function readCodexAccessToken(): string | null {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
348
|
+
const auth = readAuthFile();
|
|
349
|
+
const token = auth?.tokens?.access_token;
|
|
350
|
+
if (!token) return null;
|
|
351
|
+
const expMs = jwtExpiryMs(token);
|
|
352
|
+
if (expMs && Date.now() >= expMs) return null;
|
|
353
|
+
return token;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/* ── Helpers ── */
|
|
357
|
+
|
|
358
|
+
function stopCallbackServer(): void {
|
|
359
|
+
if (callbackServer) {
|
|
360
|
+
try { callbackServer.close(); } catch {}
|
|
361
|
+
callbackServer = null;
|
|
198
362
|
}
|
|
199
363
|
}
|
package/worker/index.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { initDb, closeDb, listConversations, createConversation, deleteConversat
|
|
|
9
9
|
import webpush from 'web-push';
|
|
10
10
|
import { TOTP } from 'otpauth';
|
|
11
11
|
import QRCode from 'qrcode';
|
|
12
|
-
import { startCodexOAuth, cancelCodexOAuth, getCodexAuthStatus
|
|
12
|
+
import { startCodexOAuth, cancelCodexOAuth, getCodexAuthStatus } from './codex-auth.js';
|
|
13
13
|
import { startClaudeOAuth, exchangeClaudeCode, getClaudeAuthStatus, readClaudeAccessToken } from './claude-auth.js';
|
|
14
14
|
import { checkAvailability, registerHandle, claimReservedHandle, releaseHandle, updateTunnelUrl, startHeartbeat, stopHeartbeat } from '../shared/relay.js';
|
|
15
15
|
import { ensureFileDirs } from '../supervisor/file-saver.js';
|
|
@@ -181,8 +181,8 @@ app.post('/api/auth/codex/cancel', (_req, res) => {
|
|
|
181
181
|
res.json({ ok: true });
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
-
app.get('/api/auth/codex/status', (_req, res) => {
|
|
185
|
-
res.json(getCodexAuthStatus());
|
|
184
|
+
app.get('/api/auth/codex/status', async (_req, res) => {
|
|
185
|
+
res.json(await getCodexAuthStatus());
|
|
186
186
|
});
|
|
187
187
|
|
|
188
188
|
// ── Claude OAuth routes ──
|
|
@@ -729,10 +729,10 @@ app.post('/api/onboard', (req, res) => {
|
|
|
729
729
|
currentCfg.ai.model = model || '';
|
|
730
730
|
currentCfg.ai.baseUrl = baseUrl || undefined;
|
|
731
731
|
|
|
732
|
-
//
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
732
|
+
// OAuth providers store their tokens in their own credentials files
|
|
733
|
+
// (~/.codex/auth.json, ~/.claude/.credentials.json) and refresh them as
|
|
734
|
+
// needed — config.ai.apiKey only holds raw API keys.
|
|
735
|
+
if (!apiKey && provider === 'anthropic') {
|
|
736
736
|
currentCfg.ai.apiKey = readClaudeAccessToken() || '';
|
|
737
737
|
} else {
|
|
738
738
|
currentCfg.ai.apiKey = apiKey || '';
|
|
@@ -335,6 +335,16 @@ MPPX_PRIVATE_KEY=$(jq -r .wallet.privateKey ~/.bloby/config.json) \
|
|
|
335
335
|
|
|
336
336
|
If you see `ACCOUNT_NOT_FOUND - No account found.`, you forgot to set `MPPX_PRIVATE_KEY` — the CLI looked in its empty keychain. Re-run with the env var.
|
|
337
337
|
|
|
338
|
+
**Buying a skill, blueprint, or bundle autonomously.** Same shape as the service call — point mppx at `/api/marketplace/buy/<productId>` instead. The relay tries your owner's credits first, then falls back to MPP from your wallet. Returns `{ skills: [{ url, sha256, ... }] }` — follow each `url` to download the .tar.gz and extract to `skills/`.
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
MPPX_PRIVATE_KEY=$(jq -r .wallet.privateKey ~/.bloby/config.json) \
|
|
342
|
+
npx -y mppx https://api.bloby.bot/api/marketplace/buy/<productId> \
|
|
343
|
+
-X POST -H "X-Bloby-Token: $RELAY_TOKEN"
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**Don't use `/marketplace/checkout/bot` for autonomous buys.** That endpoint is the cart-style flow — balance-only, no MPP fallback. If you hit it and credits are short you'll get a 402 with no way to retry. Always prefer `/marketplace/buy/:productId`.
|
|
347
|
+
|
|
338
348
|
**Wallet on disk:** `~/.bloby/config.json` field `wallet` — `address` is public, `privateKey` is secret. Never print the private key in chat. The address is fine to share.
|
|
339
349
|
|
|
340
350
|
**Running low?** Tell your human you need funds. Don't mention USDC, Tempo, or crypto unless they ask — to them, it's just adding dollars to your wallet via the "Add Funds" button in the chat header.
|
|
@@ -16,9 +16,9 @@ function DashboardError() {
|
|
|
16
16
|
playsInline
|
|
17
17
|
style={{ height: 120, width: 120, borderRadius: '50%', objectFit: 'cover', marginBottom: 32 }}
|
|
18
18
|
/>
|
|
19
|
-
<h1 style={{ fontSize: 20, fontWeight: 600, marginBottom: 8 }}>
|
|
19
|
+
<h1 style={{ fontSize: 20, fontWeight: 600, marginBottom: 8 }}>Oopss.. Something wrong is not right</h1>
|
|
20
20
|
<p style={{ fontSize: 14, color: 'rgba(255,255,255,0.5)', maxWidth: 320, lineHeight: 1.5 }}>
|
|
21
|
-
|
|
21
|
+
If your agent is working, this is normal. If not, go poke them
|
|
22
22
|
</p>
|
|
23
23
|
</div>
|
|
24
24
|
);
|