autonomo-sdk 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +105 -2
- package/dist/index.d.ts +240 -0
- package/dist/index.js +582 -0
- package/package.json +45 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Autonomo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,106 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Autonomo SDK for TypeScript
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
One call for AI agents. Say what you need, and the SDK:
|
|
4
|
+
|
|
5
|
+
1. **Finds the right API** on [Autonomo](https://autonomo.fun), from your words and the kind of input you give
|
|
6
|
+
(a token or wallet address, a transaction hash, a link).
|
|
7
|
+
2. **Pays for it** with x402 on Robinhood Chain, in USDG or $AUTONOMO, whichever the wallet can afford.
|
|
8
|
+
3. **Pays privately through [Gloam](https://gloam.trade)** when you ask, so the payment doesn't link to your wallet.
|
|
9
|
+
4. **Borrows from a [Borro](https://borro.markets) credit line** when the wallet is short of USDG.
|
|
10
|
+
5. **Keeps to your caps**: a most-per-call and a daily budget, checked before anything is signed.
|
|
11
|
+
6. **Returns the answer and a receipt**: which API, how it was paid, the amount and the transaction link.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install autonomo-sdk
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Full examples for each way to pay: [autonomo.fun/docs/sdk](https://autonomo.fun/docs/sdk).
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { Autonomo } from "autonomo-sdk";
|
|
21
|
+
|
|
22
|
+
const autonomo = new Autonomo({ privateKey: process.env.AUTONOMO_WALLET_KEY });
|
|
23
|
+
|
|
24
|
+
const { data, receipt } = await autonomo.call("is this token safe", {
|
|
25
|
+
input: "0x86b57bcb9d249cf096cf8e21426c9dc9cafdec49",
|
|
26
|
+
});
|
|
27
|
+
console.log(data);
|
|
28
|
+
console.log(receipt.route, receipt.amount, receipt.token, receipt.explorer);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Use a **new wallet just for the agent**, holding only what it may spend. A little USDG covers many calls (most APIs
|
|
32
|
+
cost 0.01 USDG). Paying in $AUTONOMO needs a little ETH once, for a Permit2 approval. Node 20 or later.
|
|
33
|
+
|
|
34
|
+
## Finding APIs without paying
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
const autonomo = new Autonomo(); // no key needed to look
|
|
38
|
+
await autonomo.find("wallet holdings"); // best matches first
|
|
39
|
+
await autonomo.quote("explain this transaction", { input: "0x…" }); // the API, the URL and every price
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
To skip the search, name the API: `autonomo.call("", { slug: "token-risk", path: "/risk/0x…" })`.
|
|
43
|
+
|
|
44
|
+
## Limits
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
new Autonomo({
|
|
48
|
+
privateKey,
|
|
49
|
+
payWith: "auto", // or "usdg" / "autonomo"
|
|
50
|
+
limits: {
|
|
51
|
+
perCall: { usdg: "0.05", autonomo: "5000" }, // the defaults
|
|
52
|
+
daily: { usdg: "1", autonomo: "10000" }, // per UTC day; Gloam and Borro count as USDG
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
A call over a limit is refused before anything is signed, with the reason. Spending is kept in
|
|
58
|
+
`~/.autonomo/spend.json` (shared with the Python SDK); pass `spendStore: memoryStore()` or your own store to change
|
|
59
|
+
that.
|
|
60
|
+
|
|
61
|
+
## Private payments with Gloam
|
|
62
|
+
|
|
63
|
+
`private: true` pays from a private Gloam balance through Gloam's own MCP server, `@gloamtrade/mcp`, running on your
|
|
64
|
+
machine with its key and limits in `~/.gloam/agent.env`. The SDK calls its `gloam_fetch_paid` tool; Gloam cashes out
|
|
65
|
+
exactly the price through its relay and calls the API with it. The SDK holds no Gloam keys, and no wallet key is needed.
|
|
66
|
+
See the [SDK guide](https://autonomo.fun/docs/sdk) for the setup and a full example.
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
const autonomo = new Autonomo({
|
|
70
|
+
gloam: {
|
|
71
|
+
fetchPaid: async (args) => {
|
|
72
|
+
const out = await gloam.callTool({ name: "gloam_fetch_paid", arguments: args }); // an MCP client for @gloamtrade/mcp
|
|
73
|
+
return JSON.parse((out.content as Array<{ text: string }>)[0].text);
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
await autonomo.call("newest launches", { private: true });
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
A private call only ever pays through Gloam. If the API doesn't offer it, or Gloam isn't set up, it's refused, never
|
|
81
|
+
sent as a public payment. The price includes Gloam's 0.5% cash-out fee.
|
|
82
|
+
|
|
83
|
+
## Borrowing with Borro
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
const autonomo = new Autonomo({ privateKey, borro: { account: "0xYourCreditLine" } });
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
When the wallet can't afford a call, the SDK asks Borro's MCP server to prepare a borrow of just what it's short,
|
|
90
|
+
sends it from the wallet (this needs a little ETH for gas), waits for the USDG and pays. The line needs x402 bills
|
|
91
|
+
turned on; see [Pay with a Borro credit line](https://autonomo.fun/docs/borro). Borro's own daily limit still applies.
|
|
92
|
+
|
|
93
|
+
## The receipt
|
|
94
|
+
|
|
95
|
+
| Field | |
|
|
96
|
+
| --- | --- |
|
|
97
|
+
| `api` | The API's slug and name |
|
|
98
|
+
| `route` | `usdg`, `autonomo`, `gloam` or `borro` |
|
|
99
|
+
| `amount`, `token` | What was paid |
|
|
100
|
+
| `payer`, `payTo` | The wallet (or "private (Gloam)") and the API's payment address |
|
|
101
|
+
| `tx`, `explorer` | The payment transaction and its Blockscout link |
|
|
102
|
+
| `borrowTxs`, `approveTx` | Borro borrows and the one-time Permit2 approval, when there were any |
|
|
103
|
+
| `spentToday` | Today's total against the daily budget |
|
|
104
|
+
|
|
105
|
+
Errors are `AutonomoError` with a `code` such as `cannot_pay`, `over_limit`, `no_private_option` or `payment_refused`.
|
|
106
|
+
If an API fails after a payment is signed, the payment doesn't settle, so you aren't charged.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { type Hex } from "viem";
|
|
2
|
+
export declare const VERSION = "0.1.0";
|
|
3
|
+
export type TokenName = "usdg" | "autonomo";
|
|
4
|
+
type Token = {
|
|
5
|
+
name: TokenName;
|
|
6
|
+
symbol: string;
|
|
7
|
+
address: Hex;
|
|
8
|
+
decimals: number;
|
|
9
|
+
};
|
|
10
|
+
export declare const TOKENS: Record<TokenName, Token>;
|
|
11
|
+
export type Limits = {
|
|
12
|
+
/** Most to pay for one call, in whole tokens, e.g. { usdg: "0.05" }. */
|
|
13
|
+
perCall?: Partial<Record<TokenName, string>>;
|
|
14
|
+
/** Most to spend per UTC day, in whole tokens. Gloam and Borro payments count as USDG. */
|
|
15
|
+
daily?: Partial<Record<TokenName, string>>;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Runs Gloam's `gloam_fetch_paid` tool (from `npx -y @gloamtrade/mcp`) with these arguments and returns its result:
|
|
19
|
+
* Gloam cashes out exactly the quoted USDG from your private balance through its relay, retries the request with the
|
|
20
|
+
* payment, and answers `{ status: "ok", hash, body, httpStatus, … }`. The SDK holds no Gloam keys.
|
|
21
|
+
*/
|
|
22
|
+
export type GloamFetchPaid = (args: {
|
|
23
|
+
url: string;
|
|
24
|
+
maxAmount: number;
|
|
25
|
+
prefer: "gloam-cashout";
|
|
26
|
+
}) => Promise<GloamResult>;
|
|
27
|
+
export type GloamResult = {
|
|
28
|
+
status: string;
|
|
29
|
+
hash?: string;
|
|
30
|
+
body?: string;
|
|
31
|
+
httpStatus?: number;
|
|
32
|
+
message?: string;
|
|
33
|
+
error?: string;
|
|
34
|
+
code?: string;
|
|
35
|
+
paymentHeader?: string;
|
|
36
|
+
};
|
|
37
|
+
/** Where today's spending is kept. The default is a JSON file in ~/.autonomo. */
|
|
38
|
+
export type SpendStore = {
|
|
39
|
+
get(day: string): Promise<Partial<Record<TokenName, bigint>>> | Partial<Record<TokenName, bigint>>;
|
|
40
|
+
add(day: string, token: TokenName, amount: bigint): Promise<void> | void;
|
|
41
|
+
};
|
|
42
|
+
export type AutonomoOptions = {
|
|
43
|
+
/** The agent wallet's private key. Use a wallet just for the agent, holding only what it may spend. */
|
|
44
|
+
privateKey?: Hex | string;
|
|
45
|
+
/** Which token to pay with. "auto" (default) tries USDG, then $AUTONOMO. */
|
|
46
|
+
payWith?: TokenName | "auto";
|
|
47
|
+
limits?: Limits;
|
|
48
|
+
/** Pay privately through Gloam when a call asks for `private: true`. */
|
|
49
|
+
gloam?: {
|
|
50
|
+
fetchPaid: GloamFetchPaid;
|
|
51
|
+
};
|
|
52
|
+
/** Borrow from a Borro credit line when the wallet is short of USDG. `account` is the line's address. */
|
|
53
|
+
borro?: {
|
|
54
|
+
account: Hex | string;
|
|
55
|
+
mcpUrl?: string;
|
|
56
|
+
};
|
|
57
|
+
spendStore?: SpendStore;
|
|
58
|
+
baseUrl?: string;
|
|
59
|
+
rpcUrl?: string;
|
|
60
|
+
fetch?: typeof fetch;
|
|
61
|
+
};
|
|
62
|
+
export type Api = {
|
|
63
|
+
slug: string;
|
|
64
|
+
name: string;
|
|
65
|
+
description: string;
|
|
66
|
+
path: string;
|
|
67
|
+
price: {
|
|
68
|
+
usdg?: number;
|
|
69
|
+
autonomo?: number;
|
|
70
|
+
};
|
|
71
|
+
verified: boolean;
|
|
72
|
+
successRate: number | null;
|
|
73
|
+
paidCalls30d: number;
|
|
74
|
+
};
|
|
75
|
+
export type CallOptions = {
|
|
76
|
+
/** The value the API needs: a token or wallet address, a transaction hash or a link. Fills the API's path. */
|
|
77
|
+
input?: string;
|
|
78
|
+
/** Call this API, skipping the search. */
|
|
79
|
+
slug?: string;
|
|
80
|
+
/** The exact path to call on that API, e.g. "/risk/0x…". */
|
|
81
|
+
path?: string;
|
|
82
|
+
/** Pay only through Gloam, so the payment doesn't link to this wallet. */
|
|
83
|
+
private?: boolean;
|
|
84
|
+
/** Override payWith for this call. */
|
|
85
|
+
payWith?: TokenName | "auto";
|
|
86
|
+
};
|
|
87
|
+
export type Route = "usdg" | "autonomo" | "gloam" | "borro";
|
|
88
|
+
export type Receipt = {
|
|
89
|
+
api: {
|
|
90
|
+
slug: string;
|
|
91
|
+
name: string;
|
|
92
|
+
};
|
|
93
|
+
url: string;
|
|
94
|
+
route: Route;
|
|
95
|
+
amount: string;
|
|
96
|
+
token: string;
|
|
97
|
+
payer: string;
|
|
98
|
+
payTo: string;
|
|
99
|
+
tx: string | null;
|
|
100
|
+
explorer: string | null;
|
|
101
|
+
borrowTxs: string[];
|
|
102
|
+
approveTx: string | null;
|
|
103
|
+
at: string;
|
|
104
|
+
spentToday: string;
|
|
105
|
+
};
|
|
106
|
+
export type CallResult<T = unknown> = {
|
|
107
|
+
data: T;
|
|
108
|
+
receipt: Receipt;
|
|
109
|
+
};
|
|
110
|
+
export declare class AutonomoError extends Error {
|
|
111
|
+
readonly code: string;
|
|
112
|
+
constructor(message: string, code: string);
|
|
113
|
+
}
|
|
114
|
+
/** Ranks APIs for a query and input: word matches first, then a fitting path, then verified and the track record. */
|
|
115
|
+
export declare function rankApis(apis: Api[], query: string, input?: string): Api[];
|
|
116
|
+
/** Keeps spending in memory only, for tests or short-lived agents. */
|
|
117
|
+
export declare function memoryStore(): SpendStore;
|
|
118
|
+
export declare class Autonomo {
|
|
119
|
+
private readonly base;
|
|
120
|
+
private readonly rpc;
|
|
121
|
+
private readonly fetch;
|
|
122
|
+
private readonly account;
|
|
123
|
+
private readonly payWith;
|
|
124
|
+
private readonly perCall;
|
|
125
|
+
private readonly daily;
|
|
126
|
+
private readonly store;
|
|
127
|
+
private readonly gloam?;
|
|
128
|
+
private readonly borro?;
|
|
129
|
+
private readonly chain;
|
|
130
|
+
constructor(opts?: AutonomoOptions);
|
|
131
|
+
/** The wallet that pays, or null when no key is set. */
|
|
132
|
+
get address(): Hex | null;
|
|
133
|
+
/** Every Autonomo API, or the best matches for a query and input. Free. */
|
|
134
|
+
find(query?: string, input?: string): Promise<Api[]>;
|
|
135
|
+
/** What a call would cost, without paying. */
|
|
136
|
+
quote(query: string, opts?: CallOptions): Promise<{
|
|
137
|
+
api: Api;
|
|
138
|
+
url: string;
|
|
139
|
+
offers: {
|
|
140
|
+
scheme: string;
|
|
141
|
+
token: string;
|
|
142
|
+
amount: string;
|
|
143
|
+
private: boolean;
|
|
144
|
+
}[];
|
|
145
|
+
}>;
|
|
146
|
+
/** Finds the right API, pays for one call and returns its answer with a receipt. */
|
|
147
|
+
call<T = unknown>(query: string, opts?: CallOptions): Promise<CallResult<T>>;
|
|
148
|
+
private get;
|
|
149
|
+
private gatewayUrl;
|
|
150
|
+
private resolve;
|
|
151
|
+
private priceList;
|
|
152
|
+
/** Signs an x402 exact payment (EIP-3009 for USDG, Permit2 for $AUTONOMO) and sends the request with it. */
|
|
153
|
+
private payExact;
|
|
154
|
+
/** Asks Borro for the borrow that covers what the wallet is short, sends it and waits for the USDG to land. */
|
|
155
|
+
private borrow;
|
|
156
|
+
}
|
|
157
|
+
export declare function eip3009TypedData(auth: {
|
|
158
|
+
from: string;
|
|
159
|
+
to: string;
|
|
160
|
+
value: string;
|
|
161
|
+
validAfter: string;
|
|
162
|
+
validBefore: string;
|
|
163
|
+
nonce: string;
|
|
164
|
+
}, asset: string, name: string, version: string): {
|
|
165
|
+
domain: {
|
|
166
|
+
name: string;
|
|
167
|
+
version: string;
|
|
168
|
+
chainId: number;
|
|
169
|
+
verifyingContract: `0x${string}`;
|
|
170
|
+
};
|
|
171
|
+
types: {
|
|
172
|
+
TransferWithAuthorization: {
|
|
173
|
+
name: string;
|
|
174
|
+
type: string;
|
|
175
|
+
}[];
|
|
176
|
+
};
|
|
177
|
+
primaryType: "TransferWithAuthorization";
|
|
178
|
+
message: {
|
|
179
|
+
from: `0x${string}`;
|
|
180
|
+
to: `0x${string}`;
|
|
181
|
+
value: bigint;
|
|
182
|
+
validAfter: bigint;
|
|
183
|
+
validBefore: bigint;
|
|
184
|
+
nonce: Hex;
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
export declare function permit2TypedData(auth: {
|
|
188
|
+
permitted: {
|
|
189
|
+
token: string;
|
|
190
|
+
amount: string;
|
|
191
|
+
};
|
|
192
|
+
spender: string;
|
|
193
|
+
nonce: string;
|
|
194
|
+
deadline: string;
|
|
195
|
+
witness: {
|
|
196
|
+
to: string;
|
|
197
|
+
validAfter: string;
|
|
198
|
+
};
|
|
199
|
+
}): {
|
|
200
|
+
domain: {
|
|
201
|
+
name: string;
|
|
202
|
+
chainId: number;
|
|
203
|
+
verifyingContract: Hex;
|
|
204
|
+
};
|
|
205
|
+
types: {
|
|
206
|
+
PermitWitnessTransferFrom: {
|
|
207
|
+
name: string;
|
|
208
|
+
type: string;
|
|
209
|
+
}[];
|
|
210
|
+
TokenPermissions: {
|
|
211
|
+
name: string;
|
|
212
|
+
type: string;
|
|
213
|
+
}[];
|
|
214
|
+
Witness: {
|
|
215
|
+
name: string;
|
|
216
|
+
type: string;
|
|
217
|
+
}[];
|
|
218
|
+
};
|
|
219
|
+
primaryType: "PermitWitnessTransferFrom";
|
|
220
|
+
message: {
|
|
221
|
+
permitted: {
|
|
222
|
+
token: `0x${string}`;
|
|
223
|
+
amount: bigint;
|
|
224
|
+
};
|
|
225
|
+
spender: `0x${string}`;
|
|
226
|
+
nonce: bigint;
|
|
227
|
+
deadline: bigint;
|
|
228
|
+
witness: {
|
|
229
|
+
to: `0x${string}`;
|
|
230
|
+
validAfter: bigint;
|
|
231
|
+
};
|
|
232
|
+
};
|
|
233
|
+
};
|
|
234
|
+
/** Every transaction ({ to, data }) inside a tool result, in order. */
|
|
235
|
+
export declare function findTransactions(value: unknown): Array<{
|
|
236
|
+
to: string;
|
|
237
|
+
data: string;
|
|
238
|
+
value?: string;
|
|
239
|
+
}>;
|
|
240
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
// Autonomo SDK: one call that finds the right paid API on Autonomo, pays for it on Robinhood Chain and returns the
|
|
2
|
+
// answer with a receipt.
|
|
3
|
+
//
|
|
4
|
+
// const autonomo = new Autonomo({ privateKey });
|
|
5
|
+
// const { data, receipt } = await autonomo.call("is this token safe", { input: "0x…" });
|
|
6
|
+
//
|
|
7
|
+
// How a call is paid, in order:
|
|
8
|
+
// private: true Only through Gloam, from a private balance, so the payment doesn't link to the wallet. Never
|
|
9
|
+
// falls back to a public payment.
|
|
10
|
+
// otherwise USDG, then $AUTONOMO (or just the one set in payWith), from the wallet, whichever it can afford.
|
|
11
|
+
// If it can't afford either and a Borro credit line is set, it borrows the USDG it's short and pays.
|
|
12
|
+
// Every route checks the per-call cap and the daily budget before anything is signed or sent.
|
|
13
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import { createPublicClient, createWalletClient, defineChain, erc20Abi, formatUnits, getAddress, http, maxUint256, parseUnits, toHex, } from "viem";
|
|
17
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
18
|
+
export const VERSION = "0.1.0";
|
|
19
|
+
const NETWORK = "eip155:4663";
|
|
20
|
+
const CHAIN_ID = 4663;
|
|
21
|
+
const EXPLORER = "https://robinhoodchain.blockscout.com";
|
|
22
|
+
const PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
|
|
23
|
+
const PERMIT2_PROXY = "0x402085c248EeA27D92E8b30b2C58ed07f9E20001"; // x402's exact-scheme Permit2 proxy
|
|
24
|
+
const BORRO_MCP = "https://borro.markets/mcp";
|
|
25
|
+
export const TOKENS = {
|
|
26
|
+
usdg: { name: "usdg", symbol: "USDG", address: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168", decimals: 6 },
|
|
27
|
+
autonomo: { name: "autonomo", symbol: "AUTONOMO", address: "0xe83c85e1ead16fcfe85401e2b337fb4fb5b5cc96", decimals: 18 },
|
|
28
|
+
};
|
|
29
|
+
const DEFAULT_LIMITS = {
|
|
30
|
+
perCall: { usdg: "0.05", autonomo: "5000" },
|
|
31
|
+
daily: { usdg: "1", autonomo: "10000" },
|
|
32
|
+
};
|
|
33
|
+
export class AutonomoError extends Error {
|
|
34
|
+
code;
|
|
35
|
+
constructor(message, code) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.code = code;
|
|
38
|
+
this.name = "AutonomoError";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const fail = (code, message) => {
|
|
42
|
+
throw new AutonomoError(message, code);
|
|
43
|
+
};
|
|
44
|
+
const b64 = (s) => Buffer.from(s, "utf8").toString("base64");
|
|
45
|
+
const unb64 = (h) => {
|
|
46
|
+
if (!h)
|
|
47
|
+
return null;
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(Buffer.from(h, "base64").toString("utf8"));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const randomHex = (bytes) => toHex(crypto.getRandomValues(new Uint8Array(bytes)));
|
|
56
|
+
const today = () => new Date().toISOString().slice(0, 10);
|
|
57
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
58
|
+
function kindOf(input) {
|
|
59
|
+
if (input === undefined)
|
|
60
|
+
return null;
|
|
61
|
+
if (/^0x[0-9a-fA-F]{64}$/.test(input))
|
|
62
|
+
return "tx";
|
|
63
|
+
if (/^0x[0-9a-fA-F]{40}$/.test(input))
|
|
64
|
+
return "address";
|
|
65
|
+
if (/^https?:\/\//i.test(input))
|
|
66
|
+
return "url";
|
|
67
|
+
return "text";
|
|
68
|
+
}
|
|
69
|
+
const placeholders = (path) => [...path.matchAll(/<([^>]+)>/g)].map((m) => m[1].toLowerCase());
|
|
70
|
+
function fits(path, kind) {
|
|
71
|
+
const ph = placeholders(path);
|
|
72
|
+
if (kind === null)
|
|
73
|
+
return ph.length === 0;
|
|
74
|
+
if (ph.length !== 1)
|
|
75
|
+
return false;
|
|
76
|
+
const p = ph[0];
|
|
77
|
+
if (kind === "tx")
|
|
78
|
+
return /transaction|hash/.test(p);
|
|
79
|
+
if (kind === "address")
|
|
80
|
+
return /address/.test(p);
|
|
81
|
+
if (kind === "url")
|
|
82
|
+
return /url|link/.test(p);
|
|
83
|
+
return !/address|hash|url|link/.test(p);
|
|
84
|
+
}
|
|
85
|
+
function fill(path, input) {
|
|
86
|
+
if (input === undefined)
|
|
87
|
+
return path;
|
|
88
|
+
return path.replace(/<([^>]+)>/, (_, p) => (/url-encoded|url|link/i.test(p) ? encodeURIComponent(input) : input));
|
|
89
|
+
}
|
|
90
|
+
/** Ranks APIs for a query and input: word matches first, then a fitting path, then verified and the track record. */
|
|
91
|
+
export function rankApis(apis, query, input) {
|
|
92
|
+
const words = query.toLowerCase().split(/[^a-z0-9$]+/).filter((w) => w.length > 2);
|
|
93
|
+
const kind = kindOf(input);
|
|
94
|
+
const scored = apis
|
|
95
|
+
.filter((a) => fits(a.path, kind))
|
|
96
|
+
.map((a) => {
|
|
97
|
+
const hay = `${a.slug} ${a.name} ${a.description}`.toLowerCase();
|
|
98
|
+
const hits = words.filter((w) => hay.includes(w)).length;
|
|
99
|
+
const trust = (a.verified ? 1 : 0) + (a.successRate ?? 0.5) + Math.min(a.paidCalls30d, 100) / 1000;
|
|
100
|
+
return { a, hits, trust };
|
|
101
|
+
})
|
|
102
|
+
.filter((s) => s.hits > 0 || words.length === 0);
|
|
103
|
+
return scored.sort((x, y) => y.hits - x.hits || y.trust - x.trust).map((s) => s.a);
|
|
104
|
+
}
|
|
105
|
+
// ---------- Spending ----------
|
|
106
|
+
function fileStore(file = join(homedir(), ".autonomo", "spend.json")) {
|
|
107
|
+
const read = () => {
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return {};
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
return {
|
|
116
|
+
get(day) {
|
|
117
|
+
const s = read();
|
|
118
|
+
if (s.date !== day)
|
|
119
|
+
return {};
|
|
120
|
+
return { usdg: BigInt(s.usdg ?? 0), autonomo: BigInt(s.autonomo ?? 0) };
|
|
121
|
+
},
|
|
122
|
+
add(day, token, amount) {
|
|
123
|
+
const s = read();
|
|
124
|
+
const cur = s.date === day ? s : { date: day };
|
|
125
|
+
cur[token] = (BigInt(cur[token] ?? 0) + amount).toString();
|
|
126
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
127
|
+
writeFileSync(file, JSON.stringify(cur));
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/** Keeps spending in memory only, for tests or short-lived agents. */
|
|
132
|
+
export function memoryStore() {
|
|
133
|
+
const days = new Map();
|
|
134
|
+
return {
|
|
135
|
+
get: (day) => ({ ...(days.get(day) ?? {}) }),
|
|
136
|
+
add: (day, token, amount) => {
|
|
137
|
+
const d = days.get(day) ?? {};
|
|
138
|
+
d[token] = (d[token] ?? 0n) + amount;
|
|
139
|
+
days.set(day, d);
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
// ---------- The client ----------
|
|
144
|
+
export class Autonomo {
|
|
145
|
+
base;
|
|
146
|
+
rpc;
|
|
147
|
+
fetch;
|
|
148
|
+
account;
|
|
149
|
+
payWith;
|
|
150
|
+
perCall;
|
|
151
|
+
daily;
|
|
152
|
+
store;
|
|
153
|
+
gloam;
|
|
154
|
+
borro;
|
|
155
|
+
chain = defineChain({
|
|
156
|
+
id: CHAIN_ID,
|
|
157
|
+
name: "Robinhood Chain",
|
|
158
|
+
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
|
|
159
|
+
rpcUrls: { default: { http: ["https://rpc.mainnet.chain.robinhood.com"] } },
|
|
160
|
+
});
|
|
161
|
+
constructor(opts = {}) {
|
|
162
|
+
this.base = (opts.baseUrl ?? "https://autonomo.fun").replace(/\/+$/, "");
|
|
163
|
+
this.rpc = opts.rpcUrl ?? "https://rpc.mainnet.chain.robinhood.com";
|
|
164
|
+
this.fetch = opts.fetch ?? globalThis.fetch;
|
|
165
|
+
const key = opts.privateKey?.trim();
|
|
166
|
+
if (key !== undefined && !/^0x[0-9a-fA-F]{64}$/.test(key))
|
|
167
|
+
fail("bad_key", "privateKey should be 0x followed by 64 hex characters.");
|
|
168
|
+
this.account = key ? privateKeyToAccount(key) : null;
|
|
169
|
+
this.payWith = opts.payWith ?? "auto";
|
|
170
|
+
const units = (t, v, d) => parseUnits(v ?? d, TOKENS[t].decimals);
|
|
171
|
+
this.perCall = {
|
|
172
|
+
usdg: units("usdg", opts.limits?.perCall?.usdg, DEFAULT_LIMITS.perCall.usdg),
|
|
173
|
+
autonomo: units("autonomo", opts.limits?.perCall?.autonomo, DEFAULT_LIMITS.perCall.autonomo),
|
|
174
|
+
};
|
|
175
|
+
this.daily = {
|
|
176
|
+
usdg: units("usdg", opts.limits?.daily?.usdg, DEFAULT_LIMITS.daily.usdg),
|
|
177
|
+
autonomo: units("autonomo", opts.limits?.daily?.autonomo, DEFAULT_LIMITS.daily.autonomo),
|
|
178
|
+
};
|
|
179
|
+
this.store = opts.spendStore ?? fileStore();
|
|
180
|
+
this.gloam = opts.gloam;
|
|
181
|
+
if (opts.borro)
|
|
182
|
+
this.borro = { account: getAddress(opts.borro.account), mcpUrl: opts.borro.mcpUrl ?? BORRO_MCP };
|
|
183
|
+
}
|
|
184
|
+
/** The wallet that pays, or null when no key is set. */
|
|
185
|
+
get address() {
|
|
186
|
+
return this.account?.address ?? null;
|
|
187
|
+
}
|
|
188
|
+
/** Every Autonomo API, or the best matches for a query and input. Free. */
|
|
189
|
+
async find(query = "", input) {
|
|
190
|
+
const res = await this.get(`${this.base}/api/v1/gateways`);
|
|
191
|
+
const body = (await res.json());
|
|
192
|
+
const apis = body.data.map((g) => ({
|
|
193
|
+
slug: g.slug,
|
|
194
|
+
name: g.name,
|
|
195
|
+
description: g.description ?? "",
|
|
196
|
+
path: g.usage ?? "/",
|
|
197
|
+
price: g.price ?? {},
|
|
198
|
+
verified: Boolean(g.verified),
|
|
199
|
+
successRate: g.trackRecord?.successRate ?? null,
|
|
200
|
+
paidCalls30d: g.trackRecord?.paidCalls ?? 0,
|
|
201
|
+
}));
|
|
202
|
+
return query || input !== undefined ? rankApis(apis, query, input) : apis;
|
|
203
|
+
}
|
|
204
|
+
/** What a call would cost, without paying. */
|
|
205
|
+
async quote(query, opts = {}) {
|
|
206
|
+
const { api, url } = await this.resolve(query, opts);
|
|
207
|
+
const required = await this.priceList(url);
|
|
208
|
+
return {
|
|
209
|
+
api,
|
|
210
|
+
url,
|
|
211
|
+
offers: required.accepts.map((a) => {
|
|
212
|
+
const t = tokenByAsset(a.asset);
|
|
213
|
+
return { scheme: a.scheme, token: t?.symbol ?? a.asset, amount: formatUnits(BigInt(a.amount), t?.decimals ?? 18), private: a.scheme === "gloam-cashout" };
|
|
214
|
+
}),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/** Finds the right API, pays for one call and returns its answer with a receipt. */
|
|
218
|
+
async call(query, opts = {}) {
|
|
219
|
+
if (!opts.private && !this.account)
|
|
220
|
+
fail("no_wallet", "No wallet set. Pass privateKey (a wallet just for this agent).");
|
|
221
|
+
const account = this.account;
|
|
222
|
+
const { api, url } = await this.resolve(query, opts);
|
|
223
|
+
const required = await this.priceList(url);
|
|
224
|
+
const day = today();
|
|
225
|
+
const spent = await this.store.get(day);
|
|
226
|
+
const pay = async (route, token, amount, send) => {
|
|
227
|
+
const { res, tx, extra } = await send();
|
|
228
|
+
if (res.status !== 200) {
|
|
229
|
+
const reason = unb64(res.headers.get("payment-required"))?.error;
|
|
230
|
+
let detail = "";
|
|
231
|
+
try {
|
|
232
|
+
const j = (await res.clone().json());
|
|
233
|
+
detail = j.message ?? j.error ?? "";
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
// Not JSON.
|
|
237
|
+
}
|
|
238
|
+
fail("payment_refused", reason ? `Payment refused: ${reason}` : `The API answered ${res.status}${detail ? `: ${detail}` : ""}. You aren't charged unless it settled.`);
|
|
239
|
+
}
|
|
240
|
+
await this.store.add(day, token.name, amount);
|
|
241
|
+
const after = (await this.store.get(day))[token.name] ?? 0n;
|
|
242
|
+
const text = await res.text();
|
|
243
|
+
let data;
|
|
244
|
+
try {
|
|
245
|
+
data = JSON.parse(text);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
data = text;
|
|
249
|
+
}
|
|
250
|
+
const receipt = {
|
|
251
|
+
api: { slug: api.slug, name: api.name },
|
|
252
|
+
url,
|
|
253
|
+
route,
|
|
254
|
+
amount: formatUnits(amount, token.decimals),
|
|
255
|
+
token: token.symbol,
|
|
256
|
+
payer: route === "gloam" ? "private (Gloam)" : account.address,
|
|
257
|
+
payTo: "",
|
|
258
|
+
tx,
|
|
259
|
+
explorer: tx ? `${EXPLORER}/tx/${tx}` : null,
|
|
260
|
+
borrowTxs: [],
|
|
261
|
+
approveTx: null,
|
|
262
|
+
at: new Date().toISOString(),
|
|
263
|
+
spentToday: `${formatUnits(after, token.decimals)} of ${formatUnits(this.daily[token.name], token.decimals)} ${token.symbol}`,
|
|
264
|
+
...extra,
|
|
265
|
+
};
|
|
266
|
+
return { data: data, receipt };
|
|
267
|
+
};
|
|
268
|
+
const within = (token, amount) => {
|
|
269
|
+
const price = `${formatUnits(amount, token.decimals)} ${token.symbol}`;
|
|
270
|
+
if (amount > this.perCall[token.name])
|
|
271
|
+
return `${price} is above the per-call cap of ${formatUnits(this.perCall[token.name], token.decimals)} ${token.symbol}`;
|
|
272
|
+
const sofar = spent[token.name] ?? 0n;
|
|
273
|
+
if (sofar + amount > this.daily[token.name])
|
|
274
|
+
return `${price} would pass today's ${token.symbol} budget (${formatUnits(sofar, token.decimals)} of ${formatUnits(this.daily[token.name], token.decimals)} spent)`;
|
|
275
|
+
return null;
|
|
276
|
+
};
|
|
277
|
+
// Private: Gloam only.
|
|
278
|
+
if (opts.private) {
|
|
279
|
+
const gloam = this.gloam ?? fail("no_gloam", "private: true needs Gloam set up (options.gloam.fetchPaid).");
|
|
280
|
+
const offer = required.accepts.find((a) => a.scheme === "gloam-cashout") ?? fail("no_private_option", `${api.name} doesn't offer private payment.`);
|
|
281
|
+
const amount = BigInt(offer.amount);
|
|
282
|
+
const why = within(TOKENS.usdg, amount);
|
|
283
|
+
if (why)
|
|
284
|
+
fail("over_limit", `Not paid: ${why}.`);
|
|
285
|
+
// Gloam asks for its own quote, whose tag can differ by under 0.001 USDG; never more than the per-call cap.
|
|
286
|
+
const most = amount + 1000n < this.perCall.usdg ? amount + 1000n : this.perCall.usdg;
|
|
287
|
+
return pay("gloam", TOKENS.usdg, amount, async () => {
|
|
288
|
+
const r = await gloam.fetchPaid({ url, maxAmount: Number(formatUnits(most, 6)), prefer: "gloam-cashout" });
|
|
289
|
+
if (r.status === "paid_not_served") {
|
|
290
|
+
await this.store.add(day, "usdg", amount);
|
|
291
|
+
fail("paid_not_served", `Paid through Gloam, but the API didn't answer yet: ${r.message ?? ""} Retry gloam_fetch_paid with this paymentHeader, don't pay again: ${r.paymentHeader ?? "(none)"}`);
|
|
292
|
+
}
|
|
293
|
+
if (r.status !== "ok")
|
|
294
|
+
fail("payment_refused", `Gloam didn't pay: ${r.message ?? r.error ?? r.code ?? r.status}`);
|
|
295
|
+
const res = new Response(r.body ?? "", { status: r.httpStatus ?? 200 });
|
|
296
|
+
return { res, tx: r.hash ?? null, extra: { payTo: getAddress(offer.payTo) } };
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
// Public: the wallet's tokens, then Borro.
|
|
300
|
+
const want = opts.payWith ?? this.payWith;
|
|
301
|
+
const order = want === "auto" ? ["usdg", "autonomo"] : [want];
|
|
302
|
+
const reasons = [];
|
|
303
|
+
const pub = createPublicClient({ chain: this.chain, transport: http(this.rpc) });
|
|
304
|
+
let usdgShort = null;
|
|
305
|
+
for (const name of order) {
|
|
306
|
+
const token = TOKENS[name];
|
|
307
|
+
const offer = required.accepts.find((a) => a.scheme === "exact" && a.network === NETWORK && sameAddr(a.asset, token.address));
|
|
308
|
+
if (!offer) {
|
|
309
|
+
reasons.push(`${api.name} doesn't take ${token.symbol}`);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const amount = BigInt(offer.amount);
|
|
313
|
+
const why = within(token, amount);
|
|
314
|
+
if (why) {
|
|
315
|
+
reasons.push(why);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const balance = await pub.readContract({ address: token.address, abi: erc20Abi, functionName: "balanceOf", args: [account.address] });
|
|
319
|
+
if (balance < amount) {
|
|
320
|
+
reasons.push(`the wallet holds ${formatUnits(balance, token.decimals)} ${token.symbol}, the call costs ${formatUnits(amount, token.decimals)}`);
|
|
321
|
+
if (name === "usdg")
|
|
322
|
+
usdgShort = { offer, amount };
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
return pay(name, token, amount, () => this.payExact(url, required, offer, account, pub));
|
|
326
|
+
}
|
|
327
|
+
if (this.borro && usdgShort) {
|
|
328
|
+
const { offer, amount } = usdgShort;
|
|
329
|
+
const borrowTxs = await this.borrow(url, offer, account, pub);
|
|
330
|
+
return pay("borro", TOKENS.usdg, amount, async () => {
|
|
331
|
+
const r = await this.payExact(url, required, offer, account, pub);
|
|
332
|
+
return { ...r, extra: { ...r.extra, borrowTxs: borrowTxs.map((h) => `${EXPLORER}/tx/${h}`) } };
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
return fail("cannot_pay", `Not paid: ${reasons.join("; ")}.`);
|
|
336
|
+
}
|
|
337
|
+
// ---------- Internals ----------
|
|
338
|
+
async get(url, timeout = 15_000) {
|
|
339
|
+
const res = await this.fetch(url, { signal: AbortSignal.timeout(timeout) });
|
|
340
|
+
if (res.status === 503)
|
|
341
|
+
fail("updating", "Autonomo is being updated right now. Try again in a few minutes.");
|
|
342
|
+
if (!res.ok)
|
|
343
|
+
fail("http_error", `${new URL(url).pathname} answered ${res.status}.`);
|
|
344
|
+
return res;
|
|
345
|
+
}
|
|
346
|
+
gatewayUrl(slug, path) {
|
|
347
|
+
if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(slug))
|
|
348
|
+
fail("bad_slug", "slug should look like token-risk.");
|
|
349
|
+
const p = path.startsWith("/") ? path : `/${path}`;
|
|
350
|
+
if (p.includes("..") || p.startsWith("//") || /[\s\\<>]/.test(p))
|
|
351
|
+
fail("bad_path", `The path ${p} isn't a plain, filled-in path.`);
|
|
352
|
+
const url = new URL(`${this.base}/x/${slug}${p}`);
|
|
353
|
+
if (url.origin !== new URL(this.base).origin || !(url.pathname === `/x/${slug}` || url.pathname.startsWith(`/x/${slug}/`))) {
|
|
354
|
+
fail("bad_path", "That path leaves the API.");
|
|
355
|
+
}
|
|
356
|
+
return url.toString();
|
|
357
|
+
}
|
|
358
|
+
async resolve(query, opts) {
|
|
359
|
+
if (opts.slug) {
|
|
360
|
+
const apis = await this.find();
|
|
361
|
+
const api = apis.find((a) => a.slug === opts.slug) ?? fail("not_found", `No Autonomo API called ${opts.slug}.`);
|
|
362
|
+
return { api, url: this.gatewayUrl(api.slug, opts.path ?? fill(api.path, opts.input)) };
|
|
363
|
+
}
|
|
364
|
+
const [api] = await this.find(query, opts.input);
|
|
365
|
+
if (!api) {
|
|
366
|
+
const kind = kindOf(opts.input);
|
|
367
|
+
fail("not_found", `No Autonomo API matches "${query}"${kind && kind !== "text" ? ` for a ${kind}` : ""}. Use find() to see them all.`);
|
|
368
|
+
}
|
|
369
|
+
return { api, url: this.gatewayUrl(api.slug, opts.path ?? fill(api.path, opts.input)) };
|
|
370
|
+
}
|
|
371
|
+
async priceList(url) {
|
|
372
|
+
const res = await this.fetch(url, { signal: AbortSignal.timeout(20_000) });
|
|
373
|
+
let required = unb64(res.headers.get("payment-required"));
|
|
374
|
+
if (!required && res.status === 402) {
|
|
375
|
+
try {
|
|
376
|
+
required = (await res.json());
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
required = null;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
await res.body?.cancel();
|
|
384
|
+
}
|
|
385
|
+
if (res.status === 503)
|
|
386
|
+
fail("updating", "This API is paused or being updated. Try again later.");
|
|
387
|
+
if (res.status !== 402 || !Array.isArray(required?.accepts))
|
|
388
|
+
fail("no_price", `Expected a 402 price list from ${url}, got ${res.status}.`);
|
|
389
|
+
return required;
|
|
390
|
+
}
|
|
391
|
+
/** Signs an x402 exact payment (EIP-3009 for USDG, Permit2 for $AUTONOMO) and sends the request with it. */
|
|
392
|
+
async payExact(url, required, offer, account, pub) {
|
|
393
|
+
let approveTx = null;
|
|
394
|
+
const now = Math.floor(Date.now() / 1000);
|
|
395
|
+
const timeout = offer.maxTimeoutSeconds ?? 300;
|
|
396
|
+
let payload;
|
|
397
|
+
if (offer.extra?.assetTransferMethod === "permit2") {
|
|
398
|
+
const allowance = await pub.readContract({ address: getAddress(offer.asset), abi: erc20Abi, functionName: "allowance", args: [account.address, PERMIT2] });
|
|
399
|
+
if (allowance < BigInt(offer.amount)) {
|
|
400
|
+
const wallet = createWalletClient({ account, chain: this.chain, transport: http(this.rpc) });
|
|
401
|
+
const hash = await wallet.writeContract({ address: getAddress(offer.asset), abi: erc20Abi, functionName: "approve", args: [PERMIT2, maxUint256] });
|
|
402
|
+
await pub.waitForTransactionReceipt({ hash });
|
|
403
|
+
approveTx = `${EXPLORER}/tx/${hash}`;
|
|
404
|
+
}
|
|
405
|
+
const auth = {
|
|
406
|
+
from: account.address,
|
|
407
|
+
permitted: { token: getAddress(offer.asset), amount: offer.amount },
|
|
408
|
+
spender: PERMIT2_PROXY,
|
|
409
|
+
nonce: BigInt(randomHex(32)).toString(),
|
|
410
|
+
deadline: String(now + timeout),
|
|
411
|
+
witness: { to: getAddress(offer.payTo), validAfter: "0" },
|
|
412
|
+
};
|
|
413
|
+
const signature = await account.signTypedData(permit2TypedData(auth));
|
|
414
|
+
payload = { signature, permit2Authorization: auth };
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
const name = offer.extra?.name;
|
|
418
|
+
const version = offer.extra?.version;
|
|
419
|
+
if (!name || !version)
|
|
420
|
+
fail("bad_offer", "The price list is missing the token's EIP-712 name and version.");
|
|
421
|
+
const auth = {
|
|
422
|
+
from: account.address,
|
|
423
|
+
to: getAddress(offer.payTo),
|
|
424
|
+
value: offer.amount,
|
|
425
|
+
validAfter: "0",
|
|
426
|
+
validBefore: String(now + timeout),
|
|
427
|
+
nonce: randomHex(32),
|
|
428
|
+
};
|
|
429
|
+
const signature = await account.signTypedData(eip3009TypedData(auth, offer.asset, name, version));
|
|
430
|
+
payload = { authorization: auth, signature };
|
|
431
|
+
}
|
|
432
|
+
const envelope = required.x402Version === 1
|
|
433
|
+
? { x402Version: 1, scheme: offer.scheme, network: offer.network, payload }
|
|
434
|
+
: { x402Version: required.x402Version, payload, extensions: required.extensions, resource: required.resource, accepted: offer };
|
|
435
|
+
const header = b64(JSON.stringify(envelope));
|
|
436
|
+
const res = await this.fetch(url, {
|
|
437
|
+
headers: required.x402Version === 1 ? { "X-PAYMENT": header } : { "PAYMENT-SIGNATURE": header },
|
|
438
|
+
signal: AbortSignal.timeout(60_000),
|
|
439
|
+
});
|
|
440
|
+
const settled = unb64(res.headers.get("payment-response") ?? res.headers.get("x-payment-response"));
|
|
441
|
+
return { res, tx: settled?.transaction ?? null, extra: { approveTx, payTo: getAddress(offer.payTo) } };
|
|
442
|
+
}
|
|
443
|
+
/** Asks Borro for the borrow that covers what the wallet is short, sends it and waits for the USDG to land. */
|
|
444
|
+
async borrow(url, offer, account, pub) {
|
|
445
|
+
const borro = this.borro;
|
|
446
|
+
const required = await this.priceList(url);
|
|
447
|
+
const raw = b64(JSON.stringify(required));
|
|
448
|
+
const result = await mcpCall(this.fetch, borro.mcpUrl, "credit_bill", {
|
|
449
|
+
wallet: account.address,
|
|
450
|
+
account: borro.account,
|
|
451
|
+
payment_required: raw,
|
|
452
|
+
max_usdg: formatUnits(this.perCall.usdg, 6),
|
|
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
|
+
const need = BigInt(offer.amount);
|
|
465
|
+
for (let i = 0; i < 20; i++) {
|
|
466
|
+
const bal = await pub.readContract({ address: TOKENS.usdg.address, abi: erc20Abi, functionName: "balanceOf", args: [account.address] });
|
|
467
|
+
if (bal >= need)
|
|
468
|
+
return hashes;
|
|
469
|
+
await sleep(1000);
|
|
470
|
+
}
|
|
471
|
+
return fail("borrow_failed", "The borrowed USDG hasn't reached the wallet yet. Nothing was paid; try again shortly.");
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
// ---------- Helpers ----------
|
|
475
|
+
const sameAddr = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
476
|
+
const tokenByAsset = (asset) => Object.values(TOKENS).find((t) => sameAddr(t.address, asset));
|
|
477
|
+
export function eip3009TypedData(auth, asset, name, version) {
|
|
478
|
+
return {
|
|
479
|
+
domain: { name, version, chainId: CHAIN_ID, verifyingContract: getAddress(asset) },
|
|
480
|
+
types: {
|
|
481
|
+
TransferWithAuthorization: [
|
|
482
|
+
{ name: "from", type: "address" },
|
|
483
|
+
{ name: "to", type: "address" },
|
|
484
|
+
{ name: "value", type: "uint256" },
|
|
485
|
+
{ name: "validAfter", type: "uint256" },
|
|
486
|
+
{ name: "validBefore", type: "uint256" },
|
|
487
|
+
{ name: "nonce", type: "bytes32" },
|
|
488
|
+
],
|
|
489
|
+
},
|
|
490
|
+
primaryType: "TransferWithAuthorization",
|
|
491
|
+
message: {
|
|
492
|
+
from: getAddress(auth.from),
|
|
493
|
+
to: getAddress(auth.to),
|
|
494
|
+
value: BigInt(auth.value),
|
|
495
|
+
validAfter: BigInt(auth.validAfter),
|
|
496
|
+
validBefore: BigInt(auth.validBefore),
|
|
497
|
+
nonce: auth.nonce,
|
|
498
|
+
},
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
export function permit2TypedData(auth) {
|
|
502
|
+
return {
|
|
503
|
+
domain: { name: "Permit2", chainId: CHAIN_ID, verifyingContract: PERMIT2 },
|
|
504
|
+
types: {
|
|
505
|
+
PermitWitnessTransferFrom: [
|
|
506
|
+
{ name: "permitted", type: "TokenPermissions" },
|
|
507
|
+
{ name: "spender", type: "address" },
|
|
508
|
+
{ name: "nonce", type: "uint256" },
|
|
509
|
+
{ name: "deadline", type: "uint256" },
|
|
510
|
+
{ name: "witness", type: "Witness" },
|
|
511
|
+
],
|
|
512
|
+
TokenPermissions: [
|
|
513
|
+
{ name: "token", type: "address" },
|
|
514
|
+
{ name: "amount", type: "uint256" },
|
|
515
|
+
],
|
|
516
|
+
Witness: [
|
|
517
|
+
{ name: "to", type: "address" },
|
|
518
|
+
{ name: "validAfter", type: "uint256" },
|
|
519
|
+
],
|
|
520
|
+
},
|
|
521
|
+
primaryType: "PermitWitnessTransferFrom",
|
|
522
|
+
message: {
|
|
523
|
+
permitted: { token: getAddress(auth.permitted.token), amount: BigInt(auth.permitted.amount) },
|
|
524
|
+
spender: getAddress(auth.spender),
|
|
525
|
+
nonce: BigInt(auth.nonce),
|
|
526
|
+
deadline: BigInt(auth.deadline),
|
|
527
|
+
witness: { to: getAddress(auth.witness.to), validAfter: BigInt(auth.witness.validAfter) },
|
|
528
|
+
},
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
/** Calls one tool on an MCP server over plain JSON-RPC and returns its result, parsed from JSON when it's JSON. */
|
|
532
|
+
async function mcpCall(f, url, name, args) {
|
|
533
|
+
const res = await f(url, {
|
|
534
|
+
method: "POST",
|
|
535
|
+
headers: { "content-type": "application/json", accept: "application/json, text/event-stream" },
|
|
536
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name, arguments: args } }),
|
|
537
|
+
signal: AbortSignal.timeout(30_000),
|
|
538
|
+
});
|
|
539
|
+
let text = await res.text();
|
|
540
|
+
if (text.startsWith("event:") || text.startsWith("data:")) {
|
|
541
|
+
text = text.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5)).join("");
|
|
542
|
+
}
|
|
543
|
+
let body;
|
|
544
|
+
try {
|
|
545
|
+
body = JSON.parse(text);
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
return fail("borro_error", `Borro answered ${res.status}.`);
|
|
549
|
+
}
|
|
550
|
+
if (body.error)
|
|
551
|
+
fail("borro_error", `Borro: ${body.error.message ?? "error"}`);
|
|
552
|
+
const out = body.result?.content?.map((c) => c.text ?? "").join("") ?? "";
|
|
553
|
+
if (body.result?.isError)
|
|
554
|
+
fail("borro_refused", `Borro: ${out}`);
|
|
555
|
+
if (body.result?.structuredContent)
|
|
556
|
+
return body.result.structuredContent;
|
|
557
|
+
try {
|
|
558
|
+
return JSON.parse(out);
|
|
559
|
+
}
|
|
560
|
+
catch {
|
|
561
|
+
return out;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
/** Every transaction ({ to, data }) inside a tool result, in order. */
|
|
565
|
+
export function findTransactions(value) {
|
|
566
|
+
const out = [];
|
|
567
|
+
const walk = (v) => {
|
|
568
|
+
if (Array.isArray(v))
|
|
569
|
+
return v.forEach(walk);
|
|
570
|
+
if (!v || typeof v !== "object")
|
|
571
|
+
return;
|
|
572
|
+
const o = v;
|
|
573
|
+
if (typeof o.to === "string" && /^0x[0-9a-fA-F]{40}$/.test(o.to) && typeof o.data === "string" && /^0x[0-9a-fA-F]*$/.test(o.data) && o.data.length > 2) {
|
|
574
|
+
out.push({ to: o.to, data: o.data, value: o.value === undefined || o.value === null ? undefined : String(o.value) });
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
// Skip the typed data to sign: its "message" holds from/to/value but no calldata.
|
|
578
|
+
Object.entries(o).forEach(([k, x]) => k !== "typed_data" && walk(x));
|
|
579
|
+
};
|
|
580
|
+
walk(value);
|
|
581
|
+
return out;
|
|
582
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autonomo-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
}
|