autonomo-sdk 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -73,8 +73,27 @@ asks before paying; `--yes` skips the question, and scripts are never asked. The
73
73
  receipt to stderr, so piping into `jq` works, and `--json` prints `{ data, receipt }` as one object. Exit codes: 0
74
74
  done, 1 failed, 2 bad usage, 3 over a limit, 4 can't pay, 5 payment refused, 6 no such API.
75
75
 
76
- After `npm install -g autonomo-sdk` the command is `autonomo`. (`npx autonomo` is someone else's package.) Private
77
- Gloam payments aren't in the command yet; use the SDK for those.
76
+ After `npm install -g autonomo-sdk` the command is `autonomo`. (`npx autonomo` is someone else's package.)
77
+
78
+ ### Paying privately from the terminal
79
+
80
+ ```bash
81
+ npx autonomo-sdk risk 0x86b57bcb9d249cf096cf8e21426c9dc9cafdec49 --private --dry-run # the private price
82
+ npx autonomo-sdk risk 0x86b57bcb9d249cf096cf8e21426c9dc9cafdec49 --private # pay from Gloam
83
+ ```
84
+
85
+ `--private` pays from your private Gloam balance, so the payment doesn't link to a wallet. The command starts Gloam's
86
+ own server (`npx -y @gloamtrade/mcp`) on your machine for that one payment and stops it afterwards.
87
+
88
+ - Set Gloam up once: its key and limits go in `~/.gloam/agent.env` and USDG goes into its private balance (see
89
+ [Pay privately with Gloam](https://autonomo.fun/docs/sdk#pay-privately-with-gloam)). The command never reads
90
+ Gloam's key, and Gloam's server is never given `AUTONOMO_WALLET_KEY`.
91
+ - No `AUTONOMO_WALLET_KEY` is needed for private calls.
92
+ - The price includes Gloam's fee and counts against `--max` and `--daily`. It can't be combined with `--pay-with`
93
+ or `--borro`.
94
+ - Paying can take a few minutes, since Gloam builds the payment's proofs on your machine.
95
+ - If Gloam paid but the API didn't answer, the error prints a payment header: run the same command again with
96
+ `--retry-header <header>` to get the answer without paying twice.
78
97
 
79
98
  ## Limits
80
99
 
@@ -126,6 +145,11 @@ When the wallet can't afford a call, the SDK asks Borro's MCP server to prepare
126
145
  sends it from the wallet (this needs a little ETH for gas), waits for the USDG and pays. The line needs x402 bills
127
146
  turned on; see [Pay with a Borro credit line](https://autonomo.fun/docs/borro). Borro's own daily limit still applies.
128
147
 
148
+ Borro's answer is checked before anything is signed. The SDK sends only one transaction: a `borrow` on your own credit
149
+ line, into this wallet, with no ETH attached, of no more than the call's price. Anything else in the answer, such as a
150
+ transfer, an approval or a second transaction, is refused with `borro_unsafe` and nothing is sent. The call's price is
151
+ still checked against your caps first.
152
+
129
153
  ## The receipt
130
154
 
131
155
  | Field | |
package/dist/cli.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { type GloamSession } from "./gloam.js";
2
3
  import { type SpendStore } from "./index.js";
3
4
  /** Everything the CLI touches outside itself, so tests can swap it. */
4
5
  export type Io = {
@@ -10,6 +11,8 @@ export type Io = {
10
11
  confirm: (question: string) => Promise<boolean>;
11
12
  fetch?: typeof fetch;
12
13
  spendStore?: SpendStore;
14
+ /** Starts Gloam's server for --private. The default runs `npx -y @gloamtrade/mcp`. */
15
+ startGloam?: () => Promise<GloamSession>;
13
16
  };
14
17
  export declare const EXIT: {
15
18
  readonly ok: 0;
package/dist/cli.js CHANGED
@@ -4,56 +4,68 @@
4
4
  // npx autonomo-sdk find "wallet holdings" look, free
5
5
  // npx autonomo-sdk risk 0x… --dry-run the price, without paying
6
6
  // AUTONOMO_WALLET_KEY=0x… npx autonomo-sdk risk 0x… pay and print the answer
7
+ // npx autonomo-sdk risk 0x… --private pay from a private Gloam balance instead
7
8
  //
8
9
  // The answer goes to stdout and everything else (the receipt, questions, errors) to stderr, so `autonomo … | jq`
9
10
  // works. The wallet key is only ever read from AUTONOMO_WALLET_KEY, never from a flag, so it stays out of shell
10
- // history. The SDK's per-call cap and daily budget apply to every paid call.
11
+ // history. The SDK's per-call cap and daily budget apply to every paid call. With --private the payment is made by
12
+ // Gloam's own server, started on this machine with its own key and limits (see gloam.ts); no wallet key is needed.
11
13
  import { realpathSync } from "node:fs";
12
14
  import { createInterface } from "node:readline/promises";
13
15
  import { fileURLToPath } from "node:url";
14
16
  import { parseArgs } from "node:util";
15
17
  import { formatUnits } from "viem";
18
+ import { startGloam } from "./gloam.js";
16
19
  import { Autonomo, AutonomoError, DEFAULT_LIMITS, TOKENS, VERSION, fileStore } from "./index.js";
17
20
  export const EXIT = { ok: 0, failed: 1, usage: 2, overLimit: 3, cannotPay: 4, refused: 5, notFound: 6 };
18
- const HELP = `Autonomo CLI ${VERSION}: find and pay for APIs on autonomo.fun
19
-
20
- Usage
21
- autonomo find [words] [input] List APIs, best matches first. Free.
22
- autonomo quote <words> [input] The API a call would use and its prices. Free.
23
- autonomo call <words> [input] Find the right API, pay for one call, print the answer.
24
- autonomo spend Today's spending against your limits.
25
-
26
- Shortcuts for the official APIs
27
- autonomo price [token address] $AUTONOMO's price, or any launchpad token's
28
- autonomo launches [how many] Newest token launches
29
- autonomo risk <token address> Rug-risk check
30
- autonomo tx <transaction hash> A transaction in plain words
31
- autonomo portfolio <wallet> What a wallet holds
32
- autonomo read <https link> A web page's text
33
- autonomo offers <what you need> Matching offers on Ostrapad
34
-
35
- Options
36
- --max <usdg> Most to pay for one call (default ${DEFAULT_LIMITS.perCall.usdg})
37
- --daily <usdg> Most to spend per UTC day (default ${DEFAULT_LIMITS.daily.usdg})
38
- --max-autonomo <amount> The same two limits when paying in $AUTONOMO
39
- --daily-autonomo <amount> (defaults ${DEFAULT_LIMITS.perCall.autonomo} and ${DEFAULT_LIMITS.daily.autonomo})
40
- --pay-with <token> usdg, autonomo or auto (default: USDG, then $AUTONOMO)
41
- --borro <credit line> Borrow from this Borro line when the wallet is short of USDG
42
- --slug <api> --path <p> With call or quote: use this API and path, skipping the search
43
- --dry-run Show the price instead of paying
44
- --yes Don't ask before paying
45
- --json One JSON object on stdout: { data, receipt }
46
- --help, --version
47
-
48
- Wallet
49
- Set AUTONOMO_WALLET_KEY to the private key of a wallet made just for the agent, holding only what it may spend.
50
- find, quote, spend and --dry-run need no wallet.
51
-
52
- Exit codes
21
+ const HELP = `Autonomo CLI ${VERSION}: find and pay for APIs on autonomo.fun
22
+
23
+ Usage
24
+ autonomo find [words] [input] List APIs, best matches first. Free.
25
+ autonomo quote <words> [input] The API a call would use and its prices. Free.
26
+ autonomo call <words> [input] Find the right API, pay for one call, print the answer.
27
+ autonomo spend Today's spending against your limits.
28
+
29
+ Shortcuts for the official APIs
30
+ autonomo price [token address] $AUTONOMO's price, or any launchpad token's
31
+ autonomo launches [how many] Newest token launches
32
+ autonomo risk <token address> Rug-risk check
33
+ autonomo tx <transaction hash> A transaction in plain words
34
+ autonomo portfolio <wallet> What a wallet holds
35
+ autonomo read <https link> A web page's text
36
+ autonomo offers <what you need> Matching offers on Ostrapad
37
+
38
+ Options
39
+ --max <usdg> Most to pay for one call (default ${DEFAULT_LIMITS.perCall.usdg})
40
+ --daily <usdg> Most to spend per UTC day (default ${DEFAULT_LIMITS.daily.usdg})
41
+ --max-autonomo <amount> The same two limits when paying in $AUTONOMO
42
+ --daily-autonomo <amount> (defaults ${DEFAULT_LIMITS.perCall.autonomo} and ${DEFAULT_LIMITS.daily.autonomo})
43
+ --pay-with <token> usdg, autonomo or auto (default: USDG, then $AUTONOMO)
44
+ --borro <credit line> Borrow from this Borro line when the wallet is short of USDG
45
+ --private Pay from your private Gloam balance, so the payment doesn't link to a wallet
46
+ --retry-header <header> With --private: finish a call Gloam paid for but the API didn't answer, without paying again
47
+ --slug <api> --path <p> With call or quote: use this API and path, skipping the search
48
+ --dry-run Show the price instead of paying
49
+ --yes Don't ask before paying
50
+ --json One JSON object on stdout: { data, receipt }
51
+ --help, --version
52
+
53
+ Wallet
54
+ Set AUTONOMO_WALLET_KEY to the private key of a wallet made just for the agent, holding only what it may spend.
55
+ find, quote, spend and --dry-run need no wallet.
56
+
57
+ Private payments
58
+ --private starts Gloam's own server on this machine (npx @gloamtrade/mcp) and has it pay in USDG. It needs no
59
+ AUTONOMO_WALLET_KEY: Gloam uses its own key and limits from ~/.gloam/agent.env, and that key is never read by
60
+ this command. Deposit USDG into Gloam's private balance first. The price includes Gloam's fee, counts against
61
+ --max and --daily, and paying can take a few minutes. Setup: autonomo.fun/docs/sdk#pay-privately-with-gloam
62
+
63
+ Exit codes
53
64
  0 done 1 failed 2 bad usage 3 over a limit 4 can't pay 5 payment refused 6 no such API`;
54
65
  const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
55
66
  const TX_HASH = /^0x[0-9a-fA-F]{64}$/;
56
67
  const AMOUNT = /^\d+(\.\d+)?$/;
68
+ const GLOAM_SETUP = "Gloam needs its own key and limits in ~/.gloam/agent.env: autonomo.fun/docs/sdk#pay-privately-with-gloam";
57
69
  class UsageError extends Error {
58
70
  }
59
71
  const usage = (message) => {
@@ -114,7 +126,7 @@ function exitFor(e) {
114
126
  // cannot_pay covers both a limit and an empty wallet; the limit ones say "cap" or "budget".
115
127
  if (e.code === "cannot_pay")
116
128
  return /per-call cap|budget/.test(e.message) && !/the wallet holds/.test(e.message) ? EXIT.overLimit : EXIT.cannotPay;
117
- if (e.code === "no_wallet" || e.code === "bad_key")
129
+ if (e.code === "no_wallet" || e.code === "bad_key" || e.code === "no_gloam" || e.code === "no_private_option")
118
130
  return EXIT.cannotPay;
119
131
  if (e.code === "payment_refused" || e.code === "paid_not_served")
120
132
  return EXIT.refused;
@@ -136,6 +148,8 @@ export async function run(argv, io) {
136
148
  "daily-autonomo": { type: "string" },
137
149
  "pay-with": { type: "string" },
138
150
  borro: { type: "string" },
151
+ private: { type: "boolean" },
152
+ "retry-header": { type: "string" },
139
153
  slug: { type: "string" },
140
154
  path: { type: "string" },
141
155
  "dry-run": { type: "boolean" },
@@ -172,6 +186,14 @@ export async function run(argv, io) {
172
186
  const borro = values.borro ?? io.env.AUTONOMO_BORRO_ACCOUNT;
173
187
  if (borro !== undefined && !ADDRESS.test(borro))
174
188
  usage("--borro takes the credit line's address.");
189
+ const retryHeader = values["retry-header"];
190
+ if (values.private && (values["pay-with"] !== undefined || values.borro !== undefined)) {
191
+ usage("--private pays in USDG from your Gloam balance, so it can't be combined with --pay-with or --borro.");
192
+ }
193
+ if (retryHeader !== undefined && !values.private)
194
+ usage("--retry-header goes with --private: it finishes a call Gloam already paid for.");
195
+ if (retryHeader !== undefined && !retryHeader.trim())
196
+ usage("--retry-header takes the paymentHeader from the earlier result.");
175
197
  const store = io.spendStore ?? fileStore();
176
198
  const client = (withWallet) => new Autonomo({
177
199
  privateKey: withWallet ? io.env.AUTONOMO_WALLET_KEY || undefined : undefined,
@@ -251,6 +273,69 @@ export async function run(argv, io) {
251
273
  io.out(` ${o.amount} ${o.token}${o.private ? " private, through Gloam" : ""}`);
252
274
  return EXIT.ok;
253
275
  }
276
+ if (values.private) {
277
+ // Gloam's server is only started once a payment is really going to be made, and always stopped afterwards.
278
+ let session;
279
+ const gloam = async (notice) => {
280
+ if (session)
281
+ return session;
282
+ if (!json)
283
+ io.err(notice);
284
+ try {
285
+ return (session = await (io.startGloam ?? (() => startGloam(io.env, undefined, VERSION)))());
286
+ }
287
+ catch (e) {
288
+ throw new AutonomoError(`${e instanceof Error ? e.message : String(e)} ${GLOAM_SETUP}`, "no_gloam");
289
+ }
290
+ };
291
+ try {
292
+ const autonomo = new Autonomo({
293
+ limits,
294
+ spendStore: store,
295
+ fetch: io.fetch,
296
+ gloam: { fetchPaid: async (args) => (await gloam("Paying privately through Gloam. This can take a few minutes…")).fetchPaid(args) },
297
+ });
298
+ if (retryHeader !== undefined) {
299
+ const q = await autonomo.quote(words, opts);
300
+ const r = await (await gloam("Asking Gloam to present the earlier payment…")).fetchPaid({ url: q.url, paymentHeader: retryHeader.trim(), prefer: "gloam-cashout" });
301
+ if (r.status !== "ok") {
302
+ throw new AutonomoError(`Not answered yet: ${r.message ?? r.error ?? r.code ?? r.status}. Nothing more was paid; try again with the same --retry-header.`, "paid_not_served");
303
+ }
304
+ let data = r.body ?? "";
305
+ try {
306
+ data = JSON.parse(String(data));
307
+ }
308
+ catch {
309
+ // Plain text is fine.
310
+ }
311
+ if (json)
312
+ return print({ data, receipt: null }), EXIT.ok;
313
+ print(data);
314
+ io.err(`Answered by ${q.api.name} with the earlier payment. Nothing more was paid.`);
315
+ return EXIT.ok;
316
+ }
317
+ if (io.interactive && !values.yes) {
318
+ const q = await autonomo.quote(words, opts);
319
+ const offer = q.offers.find((o) => o.private);
320
+ if (!offer)
321
+ throw new AutonomoError(`${q.api.name} doesn't offer private payment.`, "no_private_option");
322
+ const question = `${q.api.name} costs ${offer.amount} ${offer.token} privately through Gloam, its fee included. Pay from your Gloam balance? [y/N] `;
323
+ if (!(await io.confirm(question)))
324
+ return io.err("Not paid."), EXIT.ok;
325
+ }
326
+ const { data, receipt } = await autonomo.call(words, { ...opts, private: true });
327
+ if (json)
328
+ return print({ data, receipt }), EXIT.ok;
329
+ print(data);
330
+ io.err(`Paid ${receipt.amount} ${receipt.token} privately through Gloam to ${receipt.api.name}. Today: ${receipt.spentToday}.`);
331
+ if (receipt.explorer)
332
+ io.err(receipt.explorer);
333
+ return EXIT.ok;
334
+ }
335
+ finally {
336
+ await session?.close().catch(() => { });
337
+ }
338
+ }
254
339
  const autonomo = client(true);
255
340
  if (!autonomo.address) {
256
341
  throw new AutonomoError("No wallet set. Put the agent wallet's private key in AUTONOMO_WALLET_KEY, or add --dry-run to see the price.", "no_wallet");
@@ -0,0 +1,23 @@
1
+ import type { GloamFetchPaid, GloamResult } from "./index.js";
2
+ /** A running Gloam server. */
3
+ export type GloamSession = {
4
+ /** Pays for one request from the private balance, or presents a payment made earlier (`paymentHeader`). */
5
+ fetchPaid: (args: Parameters<GloamFetchPaid>[0] | {
6
+ url: string;
7
+ paymentHeader: string;
8
+ prefer: "gloam-cashout";
9
+ }) => Promise<GloamResult>;
10
+ close: () => Promise<void>;
11
+ };
12
+ export type GloamServer = {
13
+ command: string;
14
+ args: string[];
15
+ };
16
+ /** `npx -y @gloamtrade/mcp`. On Windows npx is a script, so it's started through cmd. */
17
+ export declare const defaultGloamServer: () => GloamServer;
18
+ /** What Gloam's server is started with: everything it may need, but never Autonomo's own wallet key or settings. */
19
+ export declare function gloamEnv(env: Record<string, string | undefined>): Record<string, string>;
20
+ export declare function startGloam(env: Record<string, string | undefined>, server?: GloamServer, version?: string, timeouts?: {
21
+ start: number;
22
+ pay: number;
23
+ }): Promise<GloamSession>;
package/dist/gloam.js ADDED
@@ -0,0 +1,118 @@
1
+ // Gloam for the CLI's --private: starts Gloam's own MCP server on this machine and calls its paid-fetch tool.
2
+ //
3
+ // The Gloam key never comes near Autonomo's code. Gloam's server reads it, with its own spending limits, from
4
+ // ~/.gloam/agent.env (see autonomo.fun/docs/sdk#pay-privately-with-gloam). This file only speaks the MCP stdio
5
+ // protocol to that server: one JSON-RPC message per line. It is written out here instead of using an MCP client
6
+ // library so `npx autonomo-sdk` stays a small download.
7
+ import { spawn } from "node:child_process";
8
+ import { createInterface } from "node:readline";
9
+ /** `npx -y @gloamtrade/mcp`. On Windows npx is a script, so it's started through cmd. */
10
+ export const defaultGloamServer = () => process.platform === "win32" ? { command: "cmd", args: ["/c", "npx", "-y", "@gloamtrade/mcp"] } : { command: "npx", args: ["-y", "@gloamtrade/mcp"] };
11
+ // The first run downloads Gloam's server, and a private payment builds its proofs on this machine.
12
+ const START_MS = 120_000;
13
+ const PAY_MS = 300_000;
14
+ /** What Gloam's server is started with: everything it may need, but never Autonomo's own wallet key or settings. */
15
+ export function gloamEnv(env) {
16
+ return Object.fromEntries(Object.entries(env).filter((e) => e[1] !== undefined && !/^AUTONOMO_/i.test(e[0])));
17
+ }
18
+ export async function startGloam(env, server = defaultGloamServer(), version = "0", timeouts = { start: START_MS, pay: PAY_MS }) {
19
+ let child;
20
+ try {
21
+ child = spawn(server.command, server.args, { env: gloamEnv(env), stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
22
+ }
23
+ catch (e) {
24
+ throw new Error(`Gloam's server couldn't be started: ${e instanceof Error ? e.message : String(e)}`);
25
+ }
26
+ // The tail of what the server printed, for when it stops before answering.
27
+ let tail = "";
28
+ child.stderr?.on("data", (chunk) => {
29
+ tail = (tail + chunk.toString("utf8")).slice(-400);
30
+ });
31
+ let nextId = 1;
32
+ let stopped = null;
33
+ const waiting = new Map();
34
+ const stop = (why) => {
35
+ stopped ??= why;
36
+ for (const w of waiting.values())
37
+ w.reject(new Error(stopped));
38
+ waiting.clear();
39
+ };
40
+ child.on("error", (e) => stop(`Gloam's server couldn't be started: ${e.message}`));
41
+ child.on("exit", (code) => stop(`Gloam's server stopped${code === null ? "" : ` (exit ${code})`}${tail.trim() ? `: ${tail.trim().split(/\r?\n/).at(-1)}` : "."}`));
42
+ // A write to a server that has gone must not crash the CLI: the exit handler reports it.
43
+ child.stdin?.on("error", () => { });
44
+ const send = (message) => child.stdin?.write(`${JSON.stringify(message)}\n`);
45
+ createInterface({ input: child.stdout }).on("line", (line) => {
46
+ let m;
47
+ try {
48
+ m = JSON.parse(line);
49
+ }
50
+ catch {
51
+ return; // Not protocol: a stray log line.
52
+ }
53
+ if (m.method !== undefined) {
54
+ // A request from the server: answer pings, decline anything else. Notifications need no answer.
55
+ if (m.id !== undefined)
56
+ send(m.method === "ping" ? { jsonrpc: "2.0", id: m.id, result: {} } : { jsonrpc: "2.0", id: m.id, error: { code: -32601, message: "Not supported" } });
57
+ return;
58
+ }
59
+ const w = m.id !== undefined ? waiting.get(m.id) : undefined;
60
+ if (!w)
61
+ return;
62
+ waiting.delete(m.id);
63
+ if (m.error)
64
+ w.reject(new Error(`Gloam: ${m.error.message ?? "request failed"}`));
65
+ else
66
+ w.resolve(m.result);
67
+ });
68
+ const request = (method, params, ms, what) => new Promise((resolve, reject) => {
69
+ if (stopped)
70
+ return reject(new Error(stopped));
71
+ const id = nextId++;
72
+ const timer = setTimeout(() => {
73
+ waiting.delete(id);
74
+ reject(new Error(`${what} took over ${Math.round(ms / 1000)} seconds.`));
75
+ }, ms);
76
+ waiting.set(id, {
77
+ resolve: (v) => (clearTimeout(timer), resolve(v)),
78
+ reject: (e) => (clearTimeout(timer), reject(e)),
79
+ });
80
+ send({ jsonrpc: "2.0", id, method, params });
81
+ });
82
+ const close = async () => {
83
+ if (child.exitCode !== null || child.signalCode !== null)
84
+ return;
85
+ const gone = new Promise((resolve) => child.once("exit", () => resolve()));
86
+ child.stdin?.end(); // An MCP server exits when its input closes.
87
+ const waited = await Promise.race([gone.then(() => true), new Promise((r) => setTimeout(() => r(false), 3000).unref())]);
88
+ if (waited)
89
+ return;
90
+ // Still there: stop it, and on Windows everything cmd started with it.
91
+ if (process.platform === "win32" && child.pid)
92
+ spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true }).on("error", () => { });
93
+ else
94
+ child.kill();
95
+ };
96
+ try {
97
+ await request("initialize", { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "autonomo-cli", version } }, timeouts.start, "Starting Gloam's server");
98
+ send({ jsonrpc: "2.0", method: "notifications/initialized" });
99
+ }
100
+ catch (e) {
101
+ await close();
102
+ throw e;
103
+ }
104
+ return {
105
+ close,
106
+ fetchPaid: async (args) => {
107
+ const out = await request("tools/call", { name: "gloam_fetch_paid", arguments: args }, timeouts.pay, "The private payment");
108
+ const text = out.content?.find((c) => typeof c.text === "string")?.text ?? "";
109
+ try {
110
+ return JSON.parse(text);
111
+ }
112
+ catch {
113
+ // Gloam answers errors, such as a missing key or an empty balance, in plain words.
114
+ return { status: "error", message: text.slice(0, 500) || "Gloam gave no answer." };
115
+ }
116
+ },
117
+ };
118
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type Hex } from "viem";
2
- export declare const VERSION = "0.1.1";
2
+ export declare const VERSION = "0.1.3";
3
3
  export type TokenName = "usdg" | "autonomo";
4
4
  type Token = {
5
5
  name: TokenName;
@@ -242,6 +242,25 @@ export declare function permit2TypedData(auth: {
242
242
  };
243
243
  };
244
244
  };
245
+ /**
246
+ * The one transaction an honest Borro `credit_bill` answer holds: "one unsigned borrow of just what it is short, from the
247
+ * line to the wallet itself". Anything else is refused before it is signed: another target, ETH value, another function,
248
+ * another recipient, more than the call's price, or more than one transaction. A borrow's USDG lands in the agent's own
249
+ * wallet, so the caps on the call still bound what can be spent.
250
+ */
251
+ export declare function checkBorrow(txs: Array<{
252
+ to: string;
253
+ data: string;
254
+ value?: string;
255
+ }>, opts: {
256
+ line: string;
257
+ wallet: string;
258
+ most: bigint;
259
+ }): {
260
+ to: Hex;
261
+ data: Hex;
262
+ amount: bigint;
263
+ };
245
264
  /** Every transaction ({ to, data }) inside a tool result, in order. */
246
265
  export declare function findTransactions(value: unknown): Array<{
247
266
  to: string;
package/dist/index.js CHANGED
@@ -13,9 +13,9 @@
13
13
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
14
14
  import { homedir } from "node:os";
15
15
  import { dirname, join } from "node:path";
16
- import { createPublicClient, createWalletClient, defineChain, erc20Abi, formatUnits, getAddress, http, maxUint256, parseUnits, toHex, } from "viem";
16
+ import { createPublicClient, createWalletClient, defineChain, erc20Abi, formatEther, formatUnits, getAddress, http, maxUint256, parseUnits, toHex, } from "viem";
17
17
  import { privateKeyToAccount } from "viem/accounts";
18
- export const VERSION = "0.1.1";
18
+ export const VERSION = "0.1.3";
19
19
  const NETWORK = "eip155:4663";
20
20
  const CHAIN_ID = 4663;
21
21
  const EXPLORER = "https://robinhoodchain.blockscout.com";
@@ -451,24 +451,22 @@ export class Autonomo {
451
451
  payment_required: raw,
452
452
  max_usdg: formatUnits(this.perCall.usdg, 6),
453
453
  });
454
- const txs = findTransactions(result);
455
- const wallet = createWalletClient({ account, chain: this.chain, transport: http(this.rpc) });
456
- const hashes = [];
457
- for (const t of txs) {
458
- const hash = await wallet.sendTransaction({ to: getAddress(t.to), data: t.data, value: t.value ? BigInt(t.value) : 0n });
459
- const r = await pub.waitForTransactionReceipt({ hash });
460
- if (r.status !== "success")
461
- fail("borrow_failed", `Borro's borrow transaction failed: ${EXPLORER}/tx/${hash}`);
462
- hashes.push(hash);
463
- }
464
454
  const need = BigInt(offer.amount);
455
+ // Borro's answer is checked before anything is signed: it may only borrow, on this line, into this wallet, at most the price.
456
+ const tx = checkBorrow(findTransactions(result), { line: borro.account, wallet: account.address, most: need });
457
+ const wallet = createWalletClient({ account, chain: this.chain, transport: http(this.rpc) });
458
+ const hash = await wallet.sendTransaction({ to: tx.to, data: tx.data, value: 0n });
459
+ const link = `${EXPLORER}/tx/${hash}`;
460
+ const r = await pub.waitForTransactionReceipt({ hash });
461
+ if (r.status !== "success")
462
+ fail("borrow_failed", `The borrow on your Borro line failed, so nothing was borrowed and the API isn't paid: ${link}`);
465
463
  for (let i = 0; i < 20; i++) {
466
464
  const bal = await pub.readContract({ address: TOKENS.usdg.address, abi: erc20Abi, functionName: "balanceOf", args: [account.address] });
467
465
  if (bal >= need)
468
- return hashes;
466
+ return [hash];
469
467
  await sleep(1000);
470
468
  }
471
- return fail("borrow_failed", "The borrowed USDG hasn't reached the wallet yet. Nothing was paid; try again shortly.");
469
+ return fail("borrow_failed", `Borrowed ${formatUnits(tx.amount, 6)} USDG on your Borro line (${link}), but it hasn't reached the wallet yet. The API isn't paid; try again shortly.`);
472
470
  }
473
471
  }
474
472
  // ---------- Helpers ----------
@@ -561,6 +559,50 @@ async function mcpCall(f, url, name, args) {
561
559
  return out;
562
560
  }
563
561
  }
562
+ /** borrow(uint256 amount, address to) on a Borro credit line. */
563
+ const BORROW_SELECTOR = "0x4b3fd148";
564
+ /**
565
+ * The one transaction an honest Borro `credit_bill` answer holds: "one unsigned borrow of just what it is short, from the
566
+ * line to the wallet itself". Anything else is refused before it is signed: another target, ETH value, another function,
567
+ * another recipient, more than the call's price, or more than one transaction. A borrow's USDG lands in the agent's own
568
+ * wallet, so the caps on the call still bound what can be spent.
569
+ */
570
+ export function checkBorrow(txs, opts) {
571
+ const refuse = (why) => fail("borro_unsafe", `Borro's answer wasn't a plain borrow from your line into this wallet (${why}), so nothing was signed or sent.`);
572
+ if (txs.length === 0)
573
+ refuse("it held no borrow transaction");
574
+ if (txs.length > 1)
575
+ refuse(`it held ${txs.length} transactions, and a borrow is one`);
576
+ const [t] = txs;
577
+ if (!sameAddr(t.to, opts.line))
578
+ refuse(`it goes to ${t.to}, not your credit line ${opts.line}`);
579
+ let value;
580
+ try {
581
+ value = t.value === undefined || t.value === "" ? 0n : BigInt(t.value);
582
+ }
583
+ catch {
584
+ return refuse(`its value "${t.value}" isn't a number`);
585
+ }
586
+ if (value !== 0n)
587
+ refuse(`it sends ${formatEther(value)} ETH`);
588
+ const data = t.data.toLowerCase();
589
+ if (!data.startsWith(BORROW_SELECTOR))
590
+ refuse(`it calls ${data.slice(0, 10)}, not borrow`);
591
+ if (data.length !== 10 + 128)
592
+ refuse("its borrow arguments aren't an amount and a recipient");
593
+ const amount = BigInt(`0x${data.slice(10, 74)}`);
594
+ const word = data.slice(74);
595
+ if (!/^0{24}/.test(word))
596
+ refuse("its recipient isn't an address");
597
+ const to = `0x${word.slice(24)}`;
598
+ if (!sameAddr(to, opts.wallet))
599
+ refuse(`it borrows into ${getAddress(to)}, not this wallet`);
600
+ if (amount === 0n)
601
+ refuse("it borrows nothing");
602
+ if (amount > opts.most)
603
+ refuse(`it borrows ${formatUnits(amount, 6)} USDG, more than the call's ${formatUnits(opts.most, 6)}`);
604
+ return { to: getAddress(t.to), data: data, amount };
605
+ }
564
606
  /** Every transaction ({ to, data }) inside a tool result, in order. */
565
607
  export function findTransactions(value) {
566
608
  const out = [];
package/package.json CHANGED
@@ -1,50 +1,50 @@
1
- {
2
- "name": "autonomo-sdk",
3
- "version": "0.1.1",
4
- "description": "One call for AI agents: find the right paid API on Autonomo, pay in USDG or $AUTONOMO (privately through Gloam, or on Borro credit), within spending caps, and get a receipt.",
5
- "homepage": "https://autonomo.fun",
6
- "author": "Autonomo (https://autonomo.fun)",
7
- "license": "MIT",
8
- "type": "module",
9
- "main": "dist/index.js",
10
- "types": "dist/index.d.ts",
11
- "exports": {
12
- ".": {
13
- "types": "./dist/index.d.ts",
14
- "import": "./dist/index.js"
15
- }
16
- },
17
- "files": [
18
- "dist",
19
- "README.md",
20
- "LICENSE"
21
- ],
22
- "engines": {
23
- "node": ">=20"
24
- },
25
- "scripts": {
26
- "build": "tsc -p .",
27
- "test": "node --test test/*.test.mjs"
28
- },
29
- "keywords": [
30
- "x402",
31
- "autonomo",
32
- "ai-agents",
33
- "robinhood-chain",
34
- "usdg",
35
- "payments",
36
- "gloam",
37
- "borro"
38
- ],
39
- "dependencies": {
40
- "viem": "2.57.12"
41
- },
42
- "devDependencies": {
43
- "@modelcontextprotocol/sdk": "^1.32.1",
44
- "@types/node": "26.6.5",
45
- "typescript": "7.0.2"
46
- },
47
- "bin": {
48
- "autonomo": "dist/cli.js"
49
- }
50
- }
1
+ {
2
+ "name": "autonomo-sdk",
3
+ "version": "0.1.3",
4
+ "description": "One call for AI agents: find the right paid API on Autonomo, pay in USDG or $AUTONOMO (privately through Gloam, or on Borro credit), within spending caps, and get a receipt.",
5
+ "homepage": "https://autonomo.fun",
6
+ "author": "Autonomo (https://autonomo.fun)",
7
+ "license": "MIT",
8
+ "type": "module",
9
+ "main": "dist/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "engines": {
23
+ "node": ">=20"
24
+ },
25
+ "scripts": {
26
+ "build": "tsc -p .",
27
+ "test": "node --test test/*.test.mjs"
28
+ },
29
+ "keywords": [
30
+ "x402",
31
+ "autonomo",
32
+ "ai-agents",
33
+ "robinhood-chain",
34
+ "usdg",
35
+ "payments",
36
+ "gloam",
37
+ "borro"
38
+ ],
39
+ "dependencies": {
40
+ "viem": "2.57.12"
41
+ },
42
+ "devDependencies": {
43
+ "@modelcontextprotocol/sdk": "^1.32.1",
44
+ "@types/node": "26.6.5",
45
+ "typescript": "7.0.2"
46
+ },
47
+ "bin": {
48
+ "autonomo": "dist/cli.js"
49
+ }
50
+ }