leash-monad 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/README.md +73 -0
- package/dist/abi.d.ts +114 -0
- package/dist/index.cjs +176 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.mjs +152 -0
- package/dist/networks.d.ts +31 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# leash-monad
|
|
2
|
+
|
|
3
|
+
Spend limits for AI agents, enforced on Monad. This package is the seller and agent side of
|
|
4
|
+
[Leash](https://leash-monad.vercel.app): one call tells you whose agent is knocking, what it may still spend
|
|
5
|
+
today, and whether a payment will go through, before you do any work.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i leash-monad viem
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Check an agent (seller)
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { checkAgent } from "leash-monad";
|
|
15
|
+
|
|
16
|
+
const v = await checkAgent(agentAddress, myAddress, "2.00"); // Monad mainnet by default
|
|
17
|
+
if (!v.ok) return res.status(403).json(v);
|
|
18
|
+
// v = { status: "OK", ok: true, remainingToday: "3.0", expiry: 1791887540,
|
|
19
|
+
// erc8004Id: "10281", account: "0xB1C6…", reason: "This agent is leashed and this payment fits its budget." }
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
| `status` | Meaning |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `OK` | Leashed, and this payment fits today's budget |
|
|
25
|
+
| `UNKNOWN_AGENT` | No leash: nobody has capped this agent |
|
|
26
|
+
| `REVOKED` | The owner revoked it |
|
|
27
|
+
| `EXPIRED` | The leash ran out |
|
|
28
|
+
| `SELLER_NOT_ALLOWED` | The owner didn't allow this agent to pay you |
|
|
29
|
+
| `OVER_CAP` | This payment would break the daily limit |
|
|
30
|
+
|
|
31
|
+
It's one `eth_call` to the `LeashHub` contract. No API key, no Leash server in the path.
|
|
32
|
+
|
|
33
|
+
## Charge agents (x402-style gate)
|
|
34
|
+
|
|
35
|
+
`leashGate` is framework-agnostic. First request: 402 with your price and a payment reference. The agent pays
|
|
36
|
+
inside its leash (one Monad transaction) and retries with the tx hash. The gate checks the `Paid` event on-chain
|
|
37
|
+
(right agent, right seller, enough money, right reference) and lets it through.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { leashGate } from "leash-monad";
|
|
41
|
+
|
|
42
|
+
app.get("/api/forecast", async (req, res) => {
|
|
43
|
+
const g = await leashGate(req, { seller: MY_ADDRESS, priceUsd: "1" });
|
|
44
|
+
if (!g.allow) return res.status(g.status).json(g.body);
|
|
45
|
+
res.json({ paid: g.paid, tx: g.tx, data: await forecast() });
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
A revoked, expired or over-cap agent gets a 403 with the reason before you've done any work.
|
|
50
|
+
|
|
51
|
+
## Pay (agent)
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { fetchWithLeash } from "leash-monad";
|
|
55
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
56
|
+
|
|
57
|
+
const agent = privateKeyToAccount(AGENT_KEY); // the session key its owner leashed
|
|
58
|
+
const res = await fetchWithLeash("https://leash-monad.vercel.app/api/forecast", agent);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
On a 402 it pays inside the leash and retries once. If the leash says no, the payment reverts on-chain and
|
|
62
|
+
nothing leaves the account.
|
|
63
|
+
|
|
64
|
+
## Networks
|
|
65
|
+
|
|
66
|
+
| | Chain | LeashHub |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `mainnet` (default) | Monad, 143 | `0xecefc8c322e2aa77327c2b4912caec04323f4f37` |
|
|
69
|
+
| `testnet` | Monad Testnet, 10143 | `0x8b427106c04e66dfc6e8d58fa4de0478a54f510a` |
|
|
70
|
+
|
|
71
|
+
Pass `{ network: "testnet" }` as the last argument to any call.
|
|
72
|
+
|
|
73
|
+
MIT
|
package/dist/abi.d.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export declare const hubAbi: readonly [{
|
|
2
|
+
readonly name: "check";
|
|
3
|
+
readonly type: "function";
|
|
4
|
+
readonly stateMutability: "view";
|
|
5
|
+
readonly inputs: readonly [{
|
|
6
|
+
readonly type: "address";
|
|
7
|
+
readonly name: "agent";
|
|
8
|
+
}, {
|
|
9
|
+
readonly type: "address";
|
|
10
|
+
readonly name: "seller";
|
|
11
|
+
}, {
|
|
12
|
+
readonly type: "uint256";
|
|
13
|
+
readonly name: "amount";
|
|
14
|
+
}];
|
|
15
|
+
readonly outputs: readonly [{
|
|
16
|
+
readonly type: "uint8";
|
|
17
|
+
readonly name: "status";
|
|
18
|
+
}, {
|
|
19
|
+
readonly type: "uint256";
|
|
20
|
+
readonly name: "remainingToday";
|
|
21
|
+
}, {
|
|
22
|
+
readonly type: "uint64";
|
|
23
|
+
readonly name: "expiry";
|
|
24
|
+
}, {
|
|
25
|
+
readonly type: "uint256";
|
|
26
|
+
readonly name: "agentId";
|
|
27
|
+
}, {
|
|
28
|
+
readonly type: "address";
|
|
29
|
+
readonly name: "account";
|
|
30
|
+
}];
|
|
31
|
+
}, {
|
|
32
|
+
readonly name: "accountOf";
|
|
33
|
+
readonly type: "function";
|
|
34
|
+
readonly stateMutability: "view";
|
|
35
|
+
readonly inputs: readonly [{
|
|
36
|
+
readonly type: "address";
|
|
37
|
+
readonly name: "agent";
|
|
38
|
+
}];
|
|
39
|
+
readonly outputs: readonly [{
|
|
40
|
+
readonly type: "address";
|
|
41
|
+
}];
|
|
42
|
+
}];
|
|
43
|
+
export declare const accountAbi: readonly [{
|
|
44
|
+
readonly name: "pay";
|
|
45
|
+
readonly type: "function";
|
|
46
|
+
readonly stateMutability: "nonpayable";
|
|
47
|
+
readonly inputs: readonly [{
|
|
48
|
+
readonly type: "address";
|
|
49
|
+
readonly name: "seller";
|
|
50
|
+
}, {
|
|
51
|
+
readonly type: "uint256";
|
|
52
|
+
readonly name: "amount";
|
|
53
|
+
}, {
|
|
54
|
+
readonly type: "bytes32";
|
|
55
|
+
readonly name: "ref";
|
|
56
|
+
}];
|
|
57
|
+
readonly outputs: readonly [];
|
|
58
|
+
}, {
|
|
59
|
+
readonly name: "leashes";
|
|
60
|
+
readonly type: "function";
|
|
61
|
+
readonly stateMutability: "view";
|
|
62
|
+
readonly inputs: readonly [{
|
|
63
|
+
readonly type: "address";
|
|
64
|
+
}];
|
|
65
|
+
readonly outputs: readonly [{
|
|
66
|
+
readonly type: "address";
|
|
67
|
+
readonly name: "token";
|
|
68
|
+
}, {
|
|
69
|
+
readonly type: "uint128";
|
|
70
|
+
readonly name: "dailyCap";
|
|
71
|
+
}, {
|
|
72
|
+
readonly type: "uint128";
|
|
73
|
+
readonly name: "spentToday";
|
|
74
|
+
}, {
|
|
75
|
+
readonly type: "uint64";
|
|
76
|
+
readonly name: "day";
|
|
77
|
+
}, {
|
|
78
|
+
readonly type: "uint64";
|
|
79
|
+
readonly name: "expiry";
|
|
80
|
+
}, {
|
|
81
|
+
readonly type: "uint64";
|
|
82
|
+
readonly name: "agentId";
|
|
83
|
+
}, {
|
|
84
|
+
readonly type: "bool";
|
|
85
|
+
readonly name: "active";
|
|
86
|
+
}, {
|
|
87
|
+
readonly type: "bool";
|
|
88
|
+
readonly name: "anySeller";
|
|
89
|
+
}];
|
|
90
|
+
}, {
|
|
91
|
+
readonly name: "Paid";
|
|
92
|
+
readonly type: "event";
|
|
93
|
+
readonly inputs: readonly [{
|
|
94
|
+
readonly type: "address";
|
|
95
|
+
readonly name: "agent";
|
|
96
|
+
readonly indexed: true;
|
|
97
|
+
}, {
|
|
98
|
+
readonly type: "address";
|
|
99
|
+
readonly name: "seller";
|
|
100
|
+
readonly indexed: true;
|
|
101
|
+
}, {
|
|
102
|
+
readonly type: "address";
|
|
103
|
+
readonly name: "token";
|
|
104
|
+
}, {
|
|
105
|
+
readonly type: "uint256";
|
|
106
|
+
readonly name: "amount";
|
|
107
|
+
}, {
|
|
108
|
+
readonly type: "bytes32";
|
|
109
|
+
readonly name: "ref";
|
|
110
|
+
}, {
|
|
111
|
+
readonly type: "uint256";
|
|
112
|
+
readonly name: "remainingToday";
|
|
113
|
+
}];
|
|
114
|
+
}];
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
STATUS: () => STATUS,
|
|
24
|
+
accountAbi: () => accountAbi,
|
|
25
|
+
checkAgent: () => checkAgent,
|
|
26
|
+
fetchWithLeash: () => fetchWithLeash,
|
|
27
|
+
hubAbi: () => hubAbi,
|
|
28
|
+
leashGate: () => leashGate,
|
|
29
|
+
networks: () => networks,
|
|
30
|
+
payWithLeash: () => payWithLeash,
|
|
31
|
+
paymentRef: () => paymentRef,
|
|
32
|
+
proofMessage: () => proofMessage
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(src_exports);
|
|
35
|
+
var import_viem2 = require("viem");
|
|
36
|
+
|
|
37
|
+
// src/networks.ts
|
|
38
|
+
var networks = {
|
|
39
|
+
mainnet: {
|
|
40
|
+
name: "Monad",
|
|
41
|
+
chainId: 143,
|
|
42
|
+
rpc: "https://rpc.monad.xyz",
|
|
43
|
+
explorer: "https://monadvision.com",
|
|
44
|
+
hub: "0xecefc8c322e2aa77327c2b4912caec04323f4f37",
|
|
45
|
+
identityRegistry: "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432",
|
|
46
|
+
testUsd: "0x4adf40e6e5113339635e6dc54ff638e7b63bbea3"
|
|
47
|
+
},
|
|
48
|
+
testnet: {
|
|
49
|
+
name: "Monad Testnet",
|
|
50
|
+
chainId: 10143,
|
|
51
|
+
rpc: "https://testnet-rpc.monad.xyz",
|
|
52
|
+
explorer: "https://testnet.monadvision.com",
|
|
53
|
+
hub: "0x8b427106c04e66dfc6e8d58fa4de0478a54f510a",
|
|
54
|
+
identityRegistry: "0x8004A818BFB912233c491871b3d84c89A494BD9e",
|
|
55
|
+
testUsd: "0xd563843fc54be3e262df4ef3281f6ad397153197"
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// src/abi.ts
|
|
60
|
+
var import_viem = require("viem");
|
|
61
|
+
var hubAbi = (0, import_viem.parseAbi)([
|
|
62
|
+
"function check(address agent, address seller, uint256 amount) view returns (uint8 status, uint256 remainingToday, uint64 expiry, uint256 agentId, address account)",
|
|
63
|
+
"function accountOf(address agent) view returns (address)"
|
|
64
|
+
]);
|
|
65
|
+
var accountAbi = (0, import_viem.parseAbi)([
|
|
66
|
+
"function pay(address seller, uint256 amount, bytes32 ref)",
|
|
67
|
+
"function leashes(address) view returns (address token, uint128 dailyCap, uint128 spentToday, uint64 day, uint64 expiry, uint64 agentId, bool active, bool anySeller)",
|
|
68
|
+
"event Paid(address indexed agent, address indexed seller, address token, uint256 amount, bytes32 ref, uint256 remainingToday)"
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
// src/index.ts
|
|
72
|
+
var STATUS = ["OK", "UNKNOWN_AGENT", "REVOKED", "EXPIRED", "SELLER_NOT_ALLOWED", "OVER_CAP"];
|
|
73
|
+
var net = (o = {}) => (typeof o.network === "string" ? networks[o.network] : o.network) ?? networks.mainnet;
|
|
74
|
+
var clientFor = (o = {}) => o.client ?? (0, import_viem2.createPublicClient)({ transport: (0, import_viem2.http)(net(o).rpc) });
|
|
75
|
+
var REASONS = {
|
|
76
|
+
OK: "This agent is leashed and this payment fits its budget.",
|
|
77
|
+
UNKNOWN_AGENT: "This agent has no leash. Its owner hasn't set a spend limit.",
|
|
78
|
+
REVOKED: "The owner revoked this agent.",
|
|
79
|
+
EXPIRED: "This agent's leash has expired.",
|
|
80
|
+
SELLER_NOT_ALLOWED: "The owner hasn't allowed this agent to pay you.",
|
|
81
|
+
OVER_CAP: "This payment would take the agent over its daily limit."
|
|
82
|
+
};
|
|
83
|
+
async function checkAgent(agent, seller, amountUsd, o = {}) {
|
|
84
|
+
const d = o.decimals ?? 6;
|
|
85
|
+
const r = await clientFor(o).readContract({
|
|
86
|
+
address: net(o).hub,
|
|
87
|
+
abi: hubAbi,
|
|
88
|
+
functionName: "check",
|
|
89
|
+
args: [agent, seller, (0, import_viem2.parseUnits)(String(amountUsd), d)]
|
|
90
|
+
});
|
|
91
|
+
const status = STATUS[r[0]];
|
|
92
|
+
return {
|
|
93
|
+
status,
|
|
94
|
+
ok: status === "OK",
|
|
95
|
+
remainingToday: (0, import_viem2.formatUnits)(r[1], d),
|
|
96
|
+
expiry: Number(r[2]),
|
|
97
|
+
erc8004Id: r[3].toString(),
|
|
98
|
+
account: r[4] === "0x0000000000000000000000000000000000000000" ? null : r[4],
|
|
99
|
+
reason: REASONS[status]
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
var paymentRef = (seller, resource, nonce) => (0, import_viem2.keccak256)((0, import_viem2.toHex)(`leash:${seller.toLowerCase()}:${resource}:${nonce}`));
|
|
103
|
+
var proofMessage = (resource, nonce) => `leash:${resource}:${nonce}`;
|
|
104
|
+
async function leashGate(req, cfg) {
|
|
105
|
+
const h = (k) => {
|
|
106
|
+
const v = req.headers[k] ?? req.headers[k.toLowerCase()];
|
|
107
|
+
return Array.isArray(v) ? v[0] : v;
|
|
108
|
+
};
|
|
109
|
+
const resource = cfg.resource ?? new URL(req.url, "http://x").pathname;
|
|
110
|
+
const agent = h("x-leash-agent");
|
|
111
|
+
const nonce = h("x-leash-nonce"), sig = h("x-leash-proof");
|
|
112
|
+
if (!agent || !nonce || !sig) {
|
|
113
|
+
return { allow: false, status: 402, body: { error: "payment_required", price: cfg.priceUsd, payTo: cfg.seller, network: net(cfg).chainId, howTo: "Send x-leash-agent, x-leash-nonce and x-leash-proof (agent signature over 'leash:<resource>:<nonce>')." } };
|
|
114
|
+
}
|
|
115
|
+
if (!await (0, import_viem2.verifyMessage)({ address: agent, message: proofMessage(resource, nonce), signature: sig })) {
|
|
116
|
+
return { allow: false, status: 403, body: { error: "bad_proof", reason: "The signature doesn't match x-leash-agent." } };
|
|
117
|
+
}
|
|
118
|
+
const ref = paymentRef(cfg.seller, resource, nonce);
|
|
119
|
+
const payment = h("x-leash-payment");
|
|
120
|
+
if (!payment) {
|
|
121
|
+
const verdict = await checkAgent(agent, cfg.seller, cfg.priceUsd, cfg);
|
|
122
|
+
return { allow: false, status: verdict.ok ? 402 : 403, body: { error: verdict.ok ? "payment_required" : "leash_refused", price: cfg.priceUsd, payTo: cfg.seller, ref, leash: verdict } };
|
|
123
|
+
}
|
|
124
|
+
const client = clientFor(cfg);
|
|
125
|
+
const rc = await client.getTransactionReceipt({ hash: payment }).catch(() => null);
|
|
126
|
+
if (!rc || rc.status !== "success") return { allow: false, status: 402, body: { error: "payment_not_found", ref } };
|
|
127
|
+
const d = cfg.decimals ?? 6, need = (0, import_viem2.parseUnits)(cfg.priceUsd, d);
|
|
128
|
+
for (const log of rc.logs) {
|
|
129
|
+
try {
|
|
130
|
+
const ev = (0, import_viem2.decodeEventLog)({ abi: accountAbi, data: log.data, topics: log.topics });
|
|
131
|
+
if (ev.eventName !== "Paid") continue;
|
|
132
|
+
const a = ev.args;
|
|
133
|
+
const account = await client.readContract({ address: net(cfg).hub, abi: hubAbi, functionName: "accountOf", args: [agent] });
|
|
134
|
+
if (log.address.toLowerCase() === account.toLowerCase() && a.agent.toLowerCase() === agent.toLowerCase() && a.seller.toLowerCase() === cfg.seller.toLowerCase() && a.amount >= need && a.ref === ref) {
|
|
135
|
+
const verdict = await checkAgent(agent, cfg.seller, "0", cfg);
|
|
136
|
+
return { allow: true, agent, paid: (0, import_viem2.formatUnits)(a.amount, d), tx: payment, verdict };
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return { allow: false, status: 402, body: { error: "payment_mismatch", ref } };
|
|
142
|
+
}
|
|
143
|
+
async function payWithLeash(agentAccount, seller, amountUsd, ref, o = {}) {
|
|
144
|
+
const n = net(o), client = clientFor(o);
|
|
145
|
+
const account = await client.readContract({ address: n.hub, abi: hubAbi, functionName: "accountOf", args: [agentAccount.address] });
|
|
146
|
+
if (account === "0x0000000000000000000000000000000000000000") throw new Error("This agent has no leash");
|
|
147
|
+
const w = (0, import_viem2.createWalletClient)({ account: agentAccount, transport: (0, import_viem2.http)(n.rpc), chain: { id: n.chainId, name: n.name, nativeCurrency: { name: "MON", symbol: "MON", decimals: 18 }, rpcUrls: { default: { http: [n.rpc] } } } });
|
|
148
|
+
const hash = await w.writeContract({ address: account, abi: accountAbi, functionName: "pay", args: [seller, (0, import_viem2.parseUnits)(amountUsd, o.decimals ?? 6), ref], gas: o.gas ?? 150000n });
|
|
149
|
+
const rc = await client.waitForTransactionReceipt({ hash });
|
|
150
|
+
if (rc.status !== "success") throw Object.assign(new Error("Refused on-chain by the leash"), { hash });
|
|
151
|
+
return hash;
|
|
152
|
+
}
|
|
153
|
+
async function fetchWithLeash(url, agentAccount, init = {}) {
|
|
154
|
+
const resource = new URL(url).pathname, nonce = (0, import_viem2.toHex)(crypto.getRandomValues(new Uint8Array(12)));
|
|
155
|
+
const proof = await agentAccount.signMessage({ message: proofMessage(resource, nonce) });
|
|
156
|
+
const headers = { ...init.headers, "x-leash-agent": agentAccount.address, "x-leash-nonce": nonce, "x-leash-proof": proof };
|
|
157
|
+
const first = await fetch(url, { ...init, headers });
|
|
158
|
+
if (first.status !== 402) return first;
|
|
159
|
+
const body = await first.json();
|
|
160
|
+
if (!body.ref || !body.payTo) return new Response(JSON.stringify(body), { status: 402 });
|
|
161
|
+
const tx = await payWithLeash(agentAccount, body.payTo, body.price, body.ref, init);
|
|
162
|
+
return fetch(url, { ...init, headers: { ...headers, "x-leash-payment": tx } });
|
|
163
|
+
}
|
|
164
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
165
|
+
0 && (module.exports = {
|
|
166
|
+
STATUS,
|
|
167
|
+
accountAbi,
|
|
168
|
+
checkAgent,
|
|
169
|
+
fetchWithLeash,
|
|
170
|
+
hubAbi,
|
|
171
|
+
leashGate,
|
|
172
|
+
networks,
|
|
173
|
+
payWithLeash,
|
|
174
|
+
paymentRef,
|
|
175
|
+
proofMessage
|
|
176
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* leash-monad: spend limits for AI agents, enforced on Monad.
|
|
3
|
+
*
|
|
4
|
+
* Seller side: checkAgent() before you serve, leashGate() as drop-in HTTP middleware (402 until paid).
|
|
5
|
+
* Agent side: payWithLeash() to pay inside the leash, fetchWithLeash() to handle a 402 automatically.
|
|
6
|
+
*/
|
|
7
|
+
import { type Address, type Hex, type PublicClient, type Account } from "viem";
|
|
8
|
+
import { networks, type LeashNetwork } from "./networks";
|
|
9
|
+
import { hubAbi, accountAbi } from "./abi";
|
|
10
|
+
export { networks, hubAbi, accountAbi };
|
|
11
|
+
export type { LeashNetwork };
|
|
12
|
+
export declare const STATUS: readonly ["OK", "UNKNOWN_AGENT", "REVOKED", "EXPIRED", "SELLER_NOT_ALLOWED", "OVER_CAP"];
|
|
13
|
+
export type LeashStatus = (typeof STATUS)[number];
|
|
14
|
+
export type Options = {
|
|
15
|
+
network?: LeashNetwork | keyof typeof networks;
|
|
16
|
+
client?: PublicClient;
|
|
17
|
+
decimals?: number;
|
|
18
|
+
};
|
|
19
|
+
export type Verdict = {
|
|
20
|
+
/** OK means this payment would go through right now. */
|
|
21
|
+
status: LeashStatus;
|
|
22
|
+
ok: boolean;
|
|
23
|
+
/** What the agent can still spend today, in dollars (string, 2+ decimals). */
|
|
24
|
+
remainingToday: string;
|
|
25
|
+
/** Unix seconds when the leash expires (0 if unknown agent). */
|
|
26
|
+
expiry: number;
|
|
27
|
+
/** The agent's ERC-8004 identity id in Monad's registry. */
|
|
28
|
+
erc8004Id: string;
|
|
29
|
+
/** The passkey-owned Leash account the agent belongs to. */
|
|
30
|
+
account: Address | null;
|
|
31
|
+
/** A one-line, human-readable reason, safe to return to the agent. */
|
|
32
|
+
reason: string;
|
|
33
|
+
};
|
|
34
|
+
/** The seller's question, answered by Monad in one read: if this agent pays me this much, will it go through? */
|
|
35
|
+
export declare function checkAgent(agent: Address, seller: Address, amountUsd: string | number, o?: Options): Promise<Verdict>;
|
|
36
|
+
/** A request reference both sides can derive: binds the payment to this seller, this resource and this nonce. */
|
|
37
|
+
export declare const paymentRef: (seller: Address, resource: string, nonce: string) => `0x${string}`;
|
|
38
|
+
/** Message an agent signs to prove it controls the agent key named in x-leash-agent. */
|
|
39
|
+
export declare const proofMessage: (resource: string, nonce: string) => string;
|
|
40
|
+
export type GateRequest = {
|
|
41
|
+
method: string;
|
|
42
|
+
url: string;
|
|
43
|
+
headers: Record<string, string | string[] | undefined>;
|
|
44
|
+
};
|
|
45
|
+
export type GateResult = {
|
|
46
|
+
allow: true;
|
|
47
|
+
agent: Address;
|
|
48
|
+
paid: string;
|
|
49
|
+
tx: Hex;
|
|
50
|
+
verdict: Verdict;
|
|
51
|
+
} | {
|
|
52
|
+
allow: false;
|
|
53
|
+
status: 402 | 403;
|
|
54
|
+
body: Record<string, unknown>;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Framework-agnostic gate. Call it with the incoming request; serve on allow, otherwise reply with status+body.
|
|
58
|
+
*
|
|
59
|
+
* 1st request: no payment header -> 402 with price, payTo, ref and the agent's live leash verdict
|
|
60
|
+
* agent pays through its Leash (one tx), then retries with x-leash-payment: <txHash>
|
|
61
|
+
* 2nd request: the gate checks the receipt's Paid event (agent, seller, amount >= price, ref) -> allow
|
|
62
|
+
*/
|
|
63
|
+
export declare function leashGate(req: GateRequest, cfg: {
|
|
64
|
+
seller: Address;
|
|
65
|
+
priceUsd: string;
|
|
66
|
+
resource?: string;
|
|
67
|
+
} & Options): Promise<GateResult>;
|
|
68
|
+
/** Pay a seller from inside the leash. Reverts on-chain (and throws here) if it would break the leash. */
|
|
69
|
+
export declare function payWithLeash(agentAccount: Account, seller: Address, amountUsd: string, ref: Hex, o?: Options & {
|
|
70
|
+
gas?: bigint;
|
|
71
|
+
}): Promise<`0x${string}`>;
|
|
72
|
+
/** fetch() for agents: on a 402 from a Leash gate, pays inside the leash and retries once. */
|
|
73
|
+
export declare function fetchWithLeash(url: string, agentAccount: Account & {
|
|
74
|
+
signMessage: NonNullable<Account["signMessage"]>;
|
|
75
|
+
}, init?: RequestInit & Options): Promise<Response>;
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import {
|
|
3
|
+
createPublicClient,
|
|
4
|
+
createWalletClient,
|
|
5
|
+
http,
|
|
6
|
+
parseUnits,
|
|
7
|
+
formatUnits,
|
|
8
|
+
keccak256,
|
|
9
|
+
toHex,
|
|
10
|
+
decodeEventLog,
|
|
11
|
+
verifyMessage
|
|
12
|
+
} from "viem";
|
|
13
|
+
|
|
14
|
+
// src/networks.ts
|
|
15
|
+
var networks = {
|
|
16
|
+
mainnet: {
|
|
17
|
+
name: "Monad",
|
|
18
|
+
chainId: 143,
|
|
19
|
+
rpc: "https://rpc.monad.xyz",
|
|
20
|
+
explorer: "https://monadvision.com",
|
|
21
|
+
hub: "0xecefc8c322e2aa77327c2b4912caec04323f4f37",
|
|
22
|
+
identityRegistry: "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432",
|
|
23
|
+
testUsd: "0x4adf40e6e5113339635e6dc54ff638e7b63bbea3"
|
|
24
|
+
},
|
|
25
|
+
testnet: {
|
|
26
|
+
name: "Monad Testnet",
|
|
27
|
+
chainId: 10143,
|
|
28
|
+
rpc: "https://testnet-rpc.monad.xyz",
|
|
29
|
+
explorer: "https://testnet.monadvision.com",
|
|
30
|
+
hub: "0x8b427106c04e66dfc6e8d58fa4de0478a54f510a",
|
|
31
|
+
identityRegistry: "0x8004A818BFB912233c491871b3d84c89A494BD9e",
|
|
32
|
+
testUsd: "0xd563843fc54be3e262df4ef3281f6ad397153197"
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// src/abi.ts
|
|
37
|
+
import { parseAbi } from "viem";
|
|
38
|
+
var hubAbi = parseAbi([
|
|
39
|
+
"function check(address agent, address seller, uint256 amount) view returns (uint8 status, uint256 remainingToday, uint64 expiry, uint256 agentId, address account)",
|
|
40
|
+
"function accountOf(address agent) view returns (address)"
|
|
41
|
+
]);
|
|
42
|
+
var accountAbi = parseAbi([
|
|
43
|
+
"function pay(address seller, uint256 amount, bytes32 ref)",
|
|
44
|
+
"function leashes(address) view returns (address token, uint128 dailyCap, uint128 spentToday, uint64 day, uint64 expiry, uint64 agentId, bool active, bool anySeller)",
|
|
45
|
+
"event Paid(address indexed agent, address indexed seller, address token, uint256 amount, bytes32 ref, uint256 remainingToday)"
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
// src/index.ts
|
|
49
|
+
var STATUS = ["OK", "UNKNOWN_AGENT", "REVOKED", "EXPIRED", "SELLER_NOT_ALLOWED", "OVER_CAP"];
|
|
50
|
+
var net = (o = {}) => (typeof o.network === "string" ? networks[o.network] : o.network) ?? networks.mainnet;
|
|
51
|
+
var clientFor = (o = {}) => o.client ?? createPublicClient({ transport: http(net(o).rpc) });
|
|
52
|
+
var REASONS = {
|
|
53
|
+
OK: "This agent is leashed and this payment fits its budget.",
|
|
54
|
+
UNKNOWN_AGENT: "This agent has no leash. Its owner hasn't set a spend limit.",
|
|
55
|
+
REVOKED: "The owner revoked this agent.",
|
|
56
|
+
EXPIRED: "This agent's leash has expired.",
|
|
57
|
+
SELLER_NOT_ALLOWED: "The owner hasn't allowed this agent to pay you.",
|
|
58
|
+
OVER_CAP: "This payment would take the agent over its daily limit."
|
|
59
|
+
};
|
|
60
|
+
async function checkAgent(agent, seller, amountUsd, o = {}) {
|
|
61
|
+
const d = o.decimals ?? 6;
|
|
62
|
+
const r = await clientFor(o).readContract({
|
|
63
|
+
address: net(o).hub,
|
|
64
|
+
abi: hubAbi,
|
|
65
|
+
functionName: "check",
|
|
66
|
+
args: [agent, seller, parseUnits(String(amountUsd), d)]
|
|
67
|
+
});
|
|
68
|
+
const status = STATUS[r[0]];
|
|
69
|
+
return {
|
|
70
|
+
status,
|
|
71
|
+
ok: status === "OK",
|
|
72
|
+
remainingToday: formatUnits(r[1], d),
|
|
73
|
+
expiry: Number(r[2]),
|
|
74
|
+
erc8004Id: r[3].toString(),
|
|
75
|
+
account: r[4] === "0x0000000000000000000000000000000000000000" ? null : r[4],
|
|
76
|
+
reason: REASONS[status]
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
var paymentRef = (seller, resource, nonce) => keccak256(toHex(`leash:${seller.toLowerCase()}:${resource}:${nonce}`));
|
|
80
|
+
var proofMessage = (resource, nonce) => `leash:${resource}:${nonce}`;
|
|
81
|
+
async function leashGate(req, cfg) {
|
|
82
|
+
const h = (k) => {
|
|
83
|
+
const v = req.headers[k] ?? req.headers[k.toLowerCase()];
|
|
84
|
+
return Array.isArray(v) ? v[0] : v;
|
|
85
|
+
};
|
|
86
|
+
const resource = cfg.resource ?? new URL(req.url, "http://x").pathname;
|
|
87
|
+
const agent = h("x-leash-agent");
|
|
88
|
+
const nonce = h("x-leash-nonce"), sig = h("x-leash-proof");
|
|
89
|
+
if (!agent || !nonce || !sig) {
|
|
90
|
+
return { allow: false, status: 402, body: { error: "payment_required", price: cfg.priceUsd, payTo: cfg.seller, network: net(cfg).chainId, howTo: "Send x-leash-agent, x-leash-nonce and x-leash-proof (agent signature over 'leash:<resource>:<nonce>')." } };
|
|
91
|
+
}
|
|
92
|
+
if (!await verifyMessage({ address: agent, message: proofMessage(resource, nonce), signature: sig })) {
|
|
93
|
+
return { allow: false, status: 403, body: { error: "bad_proof", reason: "The signature doesn't match x-leash-agent." } };
|
|
94
|
+
}
|
|
95
|
+
const ref = paymentRef(cfg.seller, resource, nonce);
|
|
96
|
+
const payment = h("x-leash-payment");
|
|
97
|
+
if (!payment) {
|
|
98
|
+
const verdict = await checkAgent(agent, cfg.seller, cfg.priceUsd, cfg);
|
|
99
|
+
return { allow: false, status: verdict.ok ? 402 : 403, body: { error: verdict.ok ? "payment_required" : "leash_refused", price: cfg.priceUsd, payTo: cfg.seller, ref, leash: verdict } };
|
|
100
|
+
}
|
|
101
|
+
const client = clientFor(cfg);
|
|
102
|
+
const rc = await client.getTransactionReceipt({ hash: payment }).catch(() => null);
|
|
103
|
+
if (!rc || rc.status !== "success") return { allow: false, status: 402, body: { error: "payment_not_found", ref } };
|
|
104
|
+
const d = cfg.decimals ?? 6, need = parseUnits(cfg.priceUsd, d);
|
|
105
|
+
for (const log of rc.logs) {
|
|
106
|
+
try {
|
|
107
|
+
const ev = decodeEventLog({ abi: accountAbi, data: log.data, topics: log.topics });
|
|
108
|
+
if (ev.eventName !== "Paid") continue;
|
|
109
|
+
const a = ev.args;
|
|
110
|
+
const account = await client.readContract({ address: net(cfg).hub, abi: hubAbi, functionName: "accountOf", args: [agent] });
|
|
111
|
+
if (log.address.toLowerCase() === account.toLowerCase() && a.agent.toLowerCase() === agent.toLowerCase() && a.seller.toLowerCase() === cfg.seller.toLowerCase() && a.amount >= need && a.ref === ref) {
|
|
112
|
+
const verdict = await checkAgent(agent, cfg.seller, "0", cfg);
|
|
113
|
+
return { allow: true, agent, paid: formatUnits(a.amount, d), tx: payment, verdict };
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { allow: false, status: 402, body: { error: "payment_mismatch", ref } };
|
|
119
|
+
}
|
|
120
|
+
async function payWithLeash(agentAccount, seller, amountUsd, ref, o = {}) {
|
|
121
|
+
const n = net(o), client = clientFor(o);
|
|
122
|
+
const account = await client.readContract({ address: n.hub, abi: hubAbi, functionName: "accountOf", args: [agentAccount.address] });
|
|
123
|
+
if (account === "0x0000000000000000000000000000000000000000") throw new Error("This agent has no leash");
|
|
124
|
+
const w = createWalletClient({ account: agentAccount, transport: http(n.rpc), chain: { id: n.chainId, name: n.name, nativeCurrency: { name: "MON", symbol: "MON", decimals: 18 }, rpcUrls: { default: { http: [n.rpc] } } } });
|
|
125
|
+
const hash = await w.writeContract({ address: account, abi: accountAbi, functionName: "pay", args: [seller, parseUnits(amountUsd, o.decimals ?? 6), ref], gas: o.gas ?? 150000n });
|
|
126
|
+
const rc = await client.waitForTransactionReceipt({ hash });
|
|
127
|
+
if (rc.status !== "success") throw Object.assign(new Error("Refused on-chain by the leash"), { hash });
|
|
128
|
+
return hash;
|
|
129
|
+
}
|
|
130
|
+
async function fetchWithLeash(url, agentAccount, init = {}) {
|
|
131
|
+
const resource = new URL(url).pathname, nonce = toHex(crypto.getRandomValues(new Uint8Array(12)));
|
|
132
|
+
const proof = await agentAccount.signMessage({ message: proofMessage(resource, nonce) });
|
|
133
|
+
const headers = { ...init.headers, "x-leash-agent": agentAccount.address, "x-leash-nonce": nonce, "x-leash-proof": proof };
|
|
134
|
+
const first = await fetch(url, { ...init, headers });
|
|
135
|
+
if (first.status !== 402) return first;
|
|
136
|
+
const body = await first.json();
|
|
137
|
+
if (!body.ref || !body.payTo) return new Response(JSON.stringify(body), { status: 402 });
|
|
138
|
+
const tx = await payWithLeash(agentAccount, body.payTo, body.price, body.ref, init);
|
|
139
|
+
return fetch(url, { ...init, headers: { ...headers, "x-leash-payment": tx } });
|
|
140
|
+
}
|
|
141
|
+
export {
|
|
142
|
+
STATUS,
|
|
143
|
+
accountAbi,
|
|
144
|
+
checkAgent,
|
|
145
|
+
fetchWithLeash,
|
|
146
|
+
hubAbi,
|
|
147
|
+
leashGate,
|
|
148
|
+
networks,
|
|
149
|
+
payWithLeash,
|
|
150
|
+
paymentRef,
|
|
151
|
+
proofMessage
|
|
152
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Address } from "viem";
|
|
2
|
+
export type LeashNetwork = {
|
|
3
|
+
name: string;
|
|
4
|
+
chainId: number;
|
|
5
|
+
rpc: string;
|
|
6
|
+
explorer: string;
|
|
7
|
+
hub: Address;
|
|
8
|
+
identityRegistry: Address;
|
|
9
|
+
testUsd?: Address;
|
|
10
|
+
};
|
|
11
|
+
/** Live deployments. Mainnet is the default everywhere in this package. */
|
|
12
|
+
export declare const networks: {
|
|
13
|
+
mainnet: {
|
|
14
|
+
name: string;
|
|
15
|
+
chainId: number;
|
|
16
|
+
rpc: string;
|
|
17
|
+
explorer: string;
|
|
18
|
+
hub: "0xecefc8c322e2aa77327c2b4912caec04323f4f37";
|
|
19
|
+
identityRegistry: "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432";
|
|
20
|
+
testUsd: "0x4adf40e6e5113339635e6dc54ff638e7b63bbea3";
|
|
21
|
+
};
|
|
22
|
+
testnet: {
|
|
23
|
+
name: string;
|
|
24
|
+
chainId: number;
|
|
25
|
+
rpc: string;
|
|
26
|
+
explorer: string;
|
|
27
|
+
hub: "0x8b427106c04e66dfc6e8d58fa4de0478a54f510a";
|
|
28
|
+
identityRegistry: "0x8004A818BFB912233c491871b3d84c89A494BD9e";
|
|
29
|
+
testUsd: "0xd563843fc54be3e262df4ef3281f6ad397153197";
|
|
30
|
+
};
|
|
31
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "leash-monad",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Spend limits for AI agents on Monad. One call tells a seller whose agent it is, what it may spend today, and whether a payment will go through.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Robert Moore",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/iamrobertmoore/leash.git",
|
|
10
|
+
"directory": "sdk"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://leash-monad.vercel.app",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"monad",
|
|
15
|
+
"ai-agents",
|
|
16
|
+
"erc-8004",
|
|
17
|
+
"x402",
|
|
18
|
+
"passkey",
|
|
19
|
+
"spend-limits",
|
|
20
|
+
"agent-payments"
|
|
21
|
+
],
|
|
22
|
+
"main": "./dist/index.cjs",
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"import": "./dist/index.mjs",
|
|
28
|
+
"require": "./dist/index.cjs"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"README.md"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "esbuild src/index.ts --bundle --format=esm --platform=neutral --external:viem --outfile=dist/index.mjs && esbuild src/index.ts --bundle --format=cjs --platform=node --external:viem --outfile=dist/index.cjs && tsc -p . --emitDeclarationOnly",
|
|
37
|
+
"test": "node --test dist/test/*.test.js"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"viem": "^2.20.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"typescript": "^5.6.0",
|
|
44
|
+
"viem": "^2.20.0",
|
|
45
|
+
"esbuild": "^0.21.0"
|
|
46
|
+
},
|
|
47
|
+
"module": "./dist/index.mjs"
|
|
48
|
+
}
|