leash-monad 0.1.2 → 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
@@ -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",
@@ -135,6 +136,10 @@ async function leashGate(req, cfg) {
135
136
  if (ev.eventName !== "Paid") continue;
136
137
  const a = ev.args;
137
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
+ }
138
143
  if (log.address.toLowerCase() === account.toLowerCase() && a.agent.toLowerCase() === agent.toLowerCase() && a.seller.toLowerCase() === cfg.seller.toLowerCase() && a.amount >= need && a.ref === ref) {
139
144
  if (cfg.claim && !await cfg.claim(payment)) {
140
145
  return { allow: false, status: 402, body: { error: "payment_used", ref, reason: "That payment was already used. Pay for this request." } };
package/dist/index.d.ts CHANGED
@@ -69,6 +69,9 @@ export type GateConfig = {
69
69
  /** Record a payment as used; return false if it was already used. Plug in your store (Redis, a DB row) so one
70
70
  * payment buys exactly one response. Without it, a payment can be replayed until it is maxPaymentAgeSeconds old. */
71
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[];
72
75
  } & Options;
73
76
  export declare function leashGate(req: GateRequest, cfg: GateConfig): Promise<GateResult>;
74
77
  /** Pay a seller from inside the leash. Reverts on-chain (and throws here) if it would break the leash. */
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",
@@ -112,6 +113,10 @@ async function leashGate(req, cfg) {
112
113
  if (ev.eventName !== "Paid") continue;
113
114
  const a = ev.args;
114
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
+ }
115
120
  if (log.address.toLowerCase() === account.toLowerCase() && a.agent.toLowerCase() === agent.toLowerCase() && a.seller.toLowerCase() === cfg.seller.toLowerCase() && a.amount >= need && a.ref === ref) {
116
121
  if (cfg.claim && !await cfg.claim(payment)) {
117
122
  return { allow: false, status: 402, body: { error: "payment_used", ref, reason: "That payment was already used. Pay for this request." } };
@@ -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.2",
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",