leash-monad 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 +20 -0
- package/dist/index.cjs +13 -1
- package/dist/index.d.ts +11 -2
- package/dist/index.mjs +13 -1
- package/dist/networks.d.ts +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,6 +48,18 @@ app.get("/api/forecast", async (req, res) => {
|
|
|
48
48
|
|
|
49
49
|
A revoked, expired or over-cap agent gets a 403 with the reason before you've done any work.
|
|
50
50
|
|
|
51
|
+
**One payment, one response.** Pass `claim` so a payment can't be replayed: it receives the payment's tx hash and
|
|
52
|
+
returns `false` if you've seen it before (a unique row in your database is enough). Payments older than
|
|
53
|
+
`maxPaymentAgeSeconds` (default 300) are refused either way, and so are payments in any token outside
|
|
54
|
+
`acceptTokens` (default: USDC and the Leash test dollar), because a leash can name any ERC-20.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
const g = await leashGate(req, { seller: MY_ADDRESS, priceUsd: "1", claim: (tx) => db.insertIfNew("payments", tx) });
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Full examples, each runnable: [Express](../examples/express-seller.mjs), [Next.js route](../examples/next-route.ts),
|
|
61
|
+
[check only](../examples/check-before-serving.mjs), [agent side](../examples/agent-pays.mjs).
|
|
62
|
+
|
|
51
63
|
## Pay (agent)
|
|
52
64
|
|
|
53
65
|
```ts
|
|
@@ -61,6 +73,14 @@ const res = await fetchWithLeash("https://leash-monad.vercel.app/api/forecast",
|
|
|
61
73
|
On a 402 it pays inside the leash and retries once. If the leash says no, the payment reverts on-chain and
|
|
62
74
|
nothing leaves the account.
|
|
63
75
|
|
|
76
|
+
## Getting a leashed agent
|
|
77
|
+
|
|
78
|
+
1. `npx leash-monad-mcp --new-key` prints a fresh agent address and key (or use any key your agent already has).
|
|
79
|
+
2. Open [leash-monad.vercel.app/own.html](https://leash-monad.vercel.app/own.html), sign in with a passkey and leash
|
|
80
|
+
that address: daily cap, sellers, expiry, demo dollars or USDC.
|
|
81
|
+
3. Use the key with `fetchWithLeash` / `payWithLeash`, or give it to an MCP client through
|
|
82
|
+
[`leash-monad-mcp`](https://www.npmjs.com/package/leash-monad-mcp).
|
|
83
|
+
|
|
64
84
|
## Networks
|
|
65
85
|
|
|
66
86
|
| | Chain | LeashHub |
|
package/dist/index.cjs
CHANGED
|
@@ -43,7 +43,8 @@ var networks = {
|
|
|
43
43
|
explorer: "https://monadvision.com",
|
|
44
44
|
hub: "0x64a489074dd6a4b3b977e5f635a178366a8c12c3",
|
|
45
45
|
identityRegistry: "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432",
|
|
46
|
-
testUsd: "0x4adf40e6e5113339635e6dc54ff638e7b63bbea3"
|
|
46
|
+
testUsd: "0x4adf40e6e5113339635e6dc54ff638e7b63bbea3",
|
|
47
|
+
usdc: "0x754704Bc059F8C67012fEd69BC8A327a5aafb603"
|
|
47
48
|
},
|
|
48
49
|
testnet: {
|
|
49
50
|
name: "Monad Testnet",
|
|
@@ -124,6 +125,10 @@ async function leashGate(req, cfg) {
|
|
|
124
125
|
const client = clientFor(cfg);
|
|
125
126
|
const rc = await client.getTransactionReceipt({ hash: payment }).catch(() => null);
|
|
126
127
|
if (!rc || rc.status !== "success") return { allow: false, status: 402, body: { error: "payment_not_found", ref } };
|
|
128
|
+
const mined = await client.getBlock({ blockNumber: rc.blockNumber });
|
|
129
|
+
if (Date.now() / 1e3 - Number(mined.timestamp) > (cfg.maxPaymentAgeSeconds ?? 300)) {
|
|
130
|
+
return { allow: false, status: 402, body: { error: "payment_expired", ref, reason: "That payment is too old to use again. Pay for this request." } };
|
|
131
|
+
}
|
|
127
132
|
const d = cfg.decimals ?? 6, need = (0, import_viem2.parseUnits)(cfg.priceUsd, d);
|
|
128
133
|
for (const log of rc.logs) {
|
|
129
134
|
try {
|
|
@@ -131,7 +136,14 @@ async function leashGate(req, cfg) {
|
|
|
131
136
|
if (ev.eventName !== "Paid") continue;
|
|
132
137
|
const a = ev.args;
|
|
133
138
|
const account = await client.readContract({ address: net(cfg).hub, abi: hubAbi, functionName: "accountOf", args: [agent] });
|
|
139
|
+
const accepted = (cfg.acceptTokens ?? [net(cfg).usdc, net(cfg).testUsd]).filter(Boolean).map((t) => t.toLowerCase());
|
|
140
|
+
if (!accepted.includes(a.token.toLowerCase())) {
|
|
141
|
+
return { allow: false, status: 402, body: { error: "token_not_accepted", ref, reason: "That payment was in a token this seller doesn't accept." } };
|
|
142
|
+
}
|
|
134
143
|
if (log.address.toLowerCase() === account.toLowerCase() && a.agent.toLowerCase() === agent.toLowerCase() && a.seller.toLowerCase() === cfg.seller.toLowerCase() && a.amount >= need && a.ref === ref) {
|
|
144
|
+
if (cfg.claim && !await cfg.claim(payment)) {
|
|
145
|
+
return { allow: false, status: 402, body: { error: "payment_used", ref, reason: "That payment was already used. Pay for this request." } };
|
|
146
|
+
}
|
|
135
147
|
const verdict = await checkAgent(agent, cfg.seller, "0", cfg);
|
|
136
148
|
return { allow: true, agent, paid: (0, import_viem2.formatUnits)(a.amount, d), tx: payment, verdict };
|
|
137
149
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -60,11 +60,20 @@ export type GateResult = {
|
|
|
60
60
|
* agent pays through its Leash (one tx), then retries with x-leash-payment: <txHash>
|
|
61
61
|
* 2nd request: the gate checks the receipt's Paid event (agent, seller, amount >= price, ref) -> allow
|
|
62
62
|
*/
|
|
63
|
-
export
|
|
63
|
+
export type GateConfig = {
|
|
64
64
|
seller: Address;
|
|
65
65
|
priceUsd: string;
|
|
66
66
|
resource?: string;
|
|
67
|
-
|
|
67
|
+
/** Refuse payments mined longer ago than this (default 300 s). Limits how long one payment proof stays usable. */
|
|
68
|
+
maxPaymentAgeSeconds?: number;
|
|
69
|
+
/** Record a payment as used; return false if it was already used. Plug in your store (Redis, a DB row) so one
|
|
70
|
+
* payment buys exactly one response. Without it, a payment can be replayed until it is maxPaymentAgeSeconds old. */
|
|
71
|
+
claim?: (tx: Hex) => boolean | Promise<boolean>;
|
|
72
|
+
/** Tokens you accept as payment. Default: the network's USDC and the Leash test dollar. A leash can name any
|
|
73
|
+
* ERC-20, so without this check an agent could "pay" in a token worth nothing. */
|
|
74
|
+
acceptTokens?: Address[];
|
|
75
|
+
} & Options;
|
|
76
|
+
export declare function leashGate(req: GateRequest, cfg: GateConfig): Promise<GateResult>;
|
|
68
77
|
/** Pay a seller from inside the leash. Reverts on-chain (and throws here) if it would break the leash. */
|
|
69
78
|
export declare function payWithLeash(agentAccount: Account, seller: Address, amountUsd: string, ref: Hex, o?: Options & {
|
|
70
79
|
gas?: bigint;
|
package/dist/index.mjs
CHANGED
|
@@ -20,7 +20,8 @@ var networks = {
|
|
|
20
20
|
explorer: "https://monadvision.com",
|
|
21
21
|
hub: "0x64a489074dd6a4b3b977e5f635a178366a8c12c3",
|
|
22
22
|
identityRegistry: "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432",
|
|
23
|
-
testUsd: "0x4adf40e6e5113339635e6dc54ff638e7b63bbea3"
|
|
23
|
+
testUsd: "0x4adf40e6e5113339635e6dc54ff638e7b63bbea3",
|
|
24
|
+
usdc: "0x754704Bc059F8C67012fEd69BC8A327a5aafb603"
|
|
24
25
|
},
|
|
25
26
|
testnet: {
|
|
26
27
|
name: "Monad Testnet",
|
|
@@ -101,6 +102,10 @@ async function leashGate(req, cfg) {
|
|
|
101
102
|
const client = clientFor(cfg);
|
|
102
103
|
const rc = await client.getTransactionReceipt({ hash: payment }).catch(() => null);
|
|
103
104
|
if (!rc || rc.status !== "success") return { allow: false, status: 402, body: { error: "payment_not_found", ref } };
|
|
105
|
+
const mined = await client.getBlock({ blockNumber: rc.blockNumber });
|
|
106
|
+
if (Date.now() / 1e3 - Number(mined.timestamp) > (cfg.maxPaymentAgeSeconds ?? 300)) {
|
|
107
|
+
return { allow: false, status: 402, body: { error: "payment_expired", ref, reason: "That payment is too old to use again. Pay for this request." } };
|
|
108
|
+
}
|
|
104
109
|
const d = cfg.decimals ?? 6, need = parseUnits(cfg.priceUsd, d);
|
|
105
110
|
for (const log of rc.logs) {
|
|
106
111
|
try {
|
|
@@ -108,7 +113,14 @@ async function leashGate(req, cfg) {
|
|
|
108
113
|
if (ev.eventName !== "Paid") continue;
|
|
109
114
|
const a = ev.args;
|
|
110
115
|
const account = await client.readContract({ address: net(cfg).hub, abi: hubAbi, functionName: "accountOf", args: [agent] });
|
|
116
|
+
const accepted = (cfg.acceptTokens ?? [net(cfg).usdc, net(cfg).testUsd]).filter(Boolean).map((t) => t.toLowerCase());
|
|
117
|
+
if (!accepted.includes(a.token.toLowerCase())) {
|
|
118
|
+
return { allow: false, status: 402, body: { error: "token_not_accepted", ref, reason: "That payment was in a token this seller doesn't accept." } };
|
|
119
|
+
}
|
|
111
120
|
if (log.address.toLowerCase() === account.toLowerCase() && a.agent.toLowerCase() === agent.toLowerCase() && a.seller.toLowerCase() === cfg.seller.toLowerCase() && a.amount >= need && a.ref === ref) {
|
|
121
|
+
if (cfg.claim && !await cfg.claim(payment)) {
|
|
122
|
+
return { allow: false, status: 402, body: { error: "payment_used", ref, reason: "That payment was already used. Pay for this request." } };
|
|
123
|
+
}
|
|
112
124
|
const verdict = await checkAgent(agent, cfg.seller, "0", cfg);
|
|
113
125
|
return { allow: true, agent, paid: formatUnits(a.amount, d), tx: payment, verdict };
|
|
114
126
|
}
|
package/dist/networks.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export type LeashNetwork = {
|
|
|
7
7
|
hub: Address;
|
|
8
8
|
identityRegistry: Address;
|
|
9
9
|
testUsd?: Address;
|
|
10
|
+
/** Circle's USDC on this network, when there is one. */
|
|
11
|
+
usdc?: Address;
|
|
10
12
|
};
|
|
11
13
|
/** Live deployments. Mainnet is the default everywhere in this package. */
|
|
12
14
|
export declare const networks: {
|
|
@@ -18,6 +20,7 @@ export declare const networks: {
|
|
|
18
20
|
hub: "0x64a489074dd6a4b3b977e5f635a178366a8c12c3";
|
|
19
21
|
identityRegistry: "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432";
|
|
20
22
|
testUsd: "0x4adf40e6e5113339635e6dc54ff638e7b63bbea3";
|
|
23
|
+
usdc: "0x754704Bc059F8C67012fEd69BC8A327a5aafb603";
|
|
21
24
|
};
|
|
22
25
|
testnet: {
|
|
23
26
|
name: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leash-monad",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
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
5
|
"license": "MIT",
|
|
6
6
|
"author": "Robert Moore",
|