agent402-tollbooth 0.10.1 → 0.10.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.
Files changed (3) hide show
  1. package/README.md +6 -4
  2. package/package.json +2 -2
  3. package/tempo.js +18 -4
package/README.md CHANGED
@@ -8,7 +8,7 @@ in USDC over the [x402 protocol](https://x402.org), over
8
8
  [MPP](https://mpp.dev) (the Machine Payments Protocol `Payment` HTTP auth
9
9
  scheme, settled through the same stack), or for free by solving a
10
10
  proof-of-work. No platform lock-in, no card processor required, no Merchant-of-Record, no signup.
11
- The first self-hostable pay-per-crawl gate that speaks both wires on one 402 -
11
+ A self-hostable pay-per-crawl gate that speaks both wires on one 402 -
12
12
  the sell side of [Agentic Finance](https://agent402.tools/agentic-finance),
13
13
  where agents pay and sites get paid, per request, with no account in between.
14
14
 
@@ -60,7 +60,9 @@ import { createTollbooth } from "agent402-tollbooth";
60
60
  const app = express();
61
61
 
62
62
  // Humans pass through; known AI crawlers get 402 and must pay or solve a PoW.
63
- app.use(createTollbooth({ price: "$0.002" }));
63
+ // payTo puts a USDC quote in the 402 (or set TOLLBOOTH_PAYTO); without it the
64
+ // 402 offers proof-of-work only.
65
+ app.use(createTollbooth({ price: "$0.002", payTo: "0xYourWallet" }));
64
66
 
65
67
  app.get("/article", (_req, res) => res.send("…your content…"));
66
68
  app.listen(3000);
@@ -77,7 +79,7 @@ The 402 body advertises both rails:
77
79
  {
78
80
  "error": "Payment Required",
79
81
  "message": "…humans browse free; bots pay in USDC via x402 or by solving a proof-of-work.",
80
- "accepts": [{ "scheme": "exact", "network": "base", "maxAmountRequired": "$0.002", "asset": "USDC", "payTo": "0x…", "resource": "/article" }],
82
+ "accepts": [{ "scheme": "exact", "network": "base", "maxAmountRequired": "$0.002", "asset": "USDC", "payTo": "0x…", "resource": "http://localhost:3000/article" }],
81
83
  "proofOfWork": { "algorithm": "sha256", "challenge": "…", "difficulty": 18, "token": "…", "rule": "Find a nonce so sha256(challenge+\":\"+nonce) has >= 18 leading zero bits; resend with header X-Pow-Solution: <token>:<nonce>" }
82
84
  }
83
85
  ```
@@ -328,7 +330,7 @@ Read by the bundled proxy / Express entry point (`index.js`):
328
330
  | `TOLLBOOTH_PAYTO` | – | Wallet address; advertises a USDC x402 quote (and, with `TOLLBOOTH_FACILITATOR_URL`, settles it) |
329
331
  | `TOLLBOOTH_FACILITATOR_URL` | – | x402 facilitator that settles your network. With `TOLLBOOTH_PAYTO` the CLI builds a real `@x402/express` middleware and takes payment over x402 **and** MPP (0.8.0). Needs `@x402/express @x402/core @x402/evm` installed; refuses to start without them |
330
332
  | `TOLLBOOTH_FACILITATOR_HEADERS` | – | Optional JSON object of auth headers sent on the facilitator's `/verify`, `/settle`, `/supported` (e.g. `{"X-API-Key":"…"}`) |
331
- | `TOLLBOOTH_CDP_API_KEY_ID` + `TOLLBOOTH_CDP_API_KEY_SECRET` | – | Settle through Coinbase's facilitator (CDP) instead of a URL (no fee is taken from the payment itself; Coinbase's facilitator is free for the first 1,000 settlements a month and $0.001 each after). Needs `npm i @coinbase/x402`. This is how a Coinbase Business account gets paid by agents: `TOLLBOOTH_PAYTO` = the account's USDC (Base) receive address. |
333
+ | `TOLLBOOTH_CDP_API_KEY_ID` + `TOLLBOOTH_CDP_API_KEY_SECRET` | – | Settle through Coinbase's facilitator (CDP) instead of a URL. Needs `npm i @coinbase/x402`. This is how a Coinbase Business account gets paid by agents: `TOLLBOOTH_PAYTO` = the account's USDC (Base) receive address. |
332
334
  | `TOLLBOOTH_PRICE` | `"$0.001"` | Advertised price per request |
333
335
  | `TOLLBOOTH_NETWORK` | `"base"` | x402 network. CLI settlement mode accepts `base`, `base-sepolia`, `polygon`, `arbitrum`, `optimism`, `avalanche`, `celo`, `sei`, `monad`, or a raw `eip155:<id>` |
334
336
  | `TOLLBOOTH_MPP` | on when `x402` set | `false` to switch MPP off |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent402-tollbooth",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
4
4
  "description": "Agent402 tollbooth: open-source, self-hostable pay-per-crawl for x402 AND MPP (the sell side of Agentic Finance): charge AI bots per request (USDC via x402, or MPP - the Machine Payments Protocol - through the same settlement, or USDG on Robinhood Chain, or free proof-of-work) while humans browse free. Drop-in Express middleware or reverse proxy that settles both wires from env alone - no Cloudflare, no Stripe, no signup.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -44,7 +44,7 @@
44
44
  "node": ">=20"
45
45
  },
46
46
  "scripts": {
47
- "test": "node test.js && node edge.test.js && node features.test.js",
47
+ "test": "node test.js && node edge.test.js && node features.test.js && node tempo.test.js",
48
48
  "demo": "node demo.js",
49
49
  "example": "node example.js"
50
50
  },
package/tempo.js CHANGED
@@ -323,12 +323,25 @@ export function candidateTxIds(signedTx) {
323
323
  return out;
324
324
  }
325
325
 
326
- const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
326
+ // TIP-20 TransferWithMemo(address indexed from, address indexed to, uint256 amount, bytes32 indexed memo)
327
+ const TRANSFER_WITH_MEMO_TOPIC = keccak256Hex(Buffer.from("TransferWithMemo(address,address,uint256,bytes32)"));
328
+ const MPP_TAG = keccak256Hex(Buffer.from("mpp")).slice(2, 10);
329
+
330
+ /** Is this bytes32 memo an MPP attribution memo bound to `challengeId`?
331
+ * Mirrors mppx tempo/Attribution.js verifyChallengeBinding: TAG, version
332
+ * 0x01, and bytes 25..31 = keccak256(challengeId)[0..6]. */
333
+ export function memoBoundToChallenge(memo, challengeId) {
334
+ const hex = String(memo || "").toLowerCase().replace(/^0x/, "");
335
+ if (hex.length !== 64 || typeof challengeId !== "string" || !challengeId) return false;
336
+ if (hex.slice(0, 8) !== MPP_TAG || hex.slice(8, 10) !== "01") return false;
337
+ return hex.slice(50, 64) === keccak256Hex(Buffer.from(challengeId)).slice(2, 16);
338
+ }
327
339
 
328
340
  /** Did this credential's transaction settle despite the relay's verdict?
329
341
  * Returns { txId } when a candidate receipt exists, succeeded (status 0x1),
330
- * and carries the challenge's transfer (currency + recipient + >= amount) —
331
- * else null. Polls briefly (a just-mined tx may not be indexed when the
342
+ * and carries the challenge's transfer (currency + recipient + >= amount) as
343
+ * a TransferWithMemo whose MPP memo is bound to THIS challenge's id - else
344
+ * null. Polls briefly (a just-mined tx may not be indexed when the
332
345
  * relay answers). Never throws. */
333
346
  export async function confirmTempoSettlement(credential, { rpcUrl, fetchImpl = globalThis.fetch, attempts = 4, delayMs = 2000 } = {}) {
334
347
  try {
@@ -357,8 +370,9 @@ export async function confirmTempoSettlement(credential, { rpcUrl, fetchImpl = g
357
370
  if (!receipt || receipt.status !== "0x1") continue;
358
371
  for (const log of receipt.logs || []) {
359
372
  if (String(log.address || "").toLowerCase() !== currency) continue;
360
- if ((log.topics || [])[0] !== TRANSFER_TOPIC) continue;
373
+ if ((log.topics || [])[0] !== TRANSFER_WITH_MEMO_TOPIC) continue;
361
374
  if (`0x${String(log.topics[2] || "").slice(-40)}`.toLowerCase() !== recipient) continue;
375
+ if (!memoBoundToChallenge(log.topics[3], credential?.challenge?.id)) continue;
362
376
  let value;
363
377
  try { value = BigInt(log.data); } catch { continue; }
364
378
  if (value >= minAmount) return { txId };