@paydlux/agent 0.1.11 → 0.1.13

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 +23 -2
  2. package/dist/cli.js +53 -1
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -213,11 +213,32 @@ if (res.ok) console.log(res.data, "paid in tx", res.tx);
213
213
  `npx tsx stellar/onboard.ts` creates the vault and prints that key. The key is
214
214
  **one of two signatures** — it cannot spend alone.
215
215
 
216
+ ### Your spending mandate (one command, enforced at the approve button)
217
+
218
+ ```bash
219
+ paydlux sell --price 4.50 --max 10 --daily 20
220
+ ```
221
+
222
+ No payment above 10, nothing past 20 in a day. The desk checks this **before**
223
+ it asks your phone, and again at submission — the budget can be spent between
224
+ your approval and the ledger call, so a stale signature is refused rather than
225
+ honoured. Approvals already waiting count against the budget, so a queue of six
226
+ 4.50 requests against a 20/day cap sends four to your phone and refuses two at
227
+ the button instead of letting you approve six and watch two fail.
228
+
229
+ A refusal answers the buyer with `error: "mandate_refused"` on the **same**
230
+ handle, so a polite agent cannot park-refuse-park forever. `paydlux sell` also
231
+ refuses to start if `--price` is above `--max` or `--daily`, because that
232
+ combination blocks every payment and looks like a bug in the desk.
233
+
234
+ Verified on the published package: 6 x 4.50 against `--daily 20` -> four
235
+ settled, two refused, vault balance moved exactly 18.00 and stopped.
236
+
216
237
  ### What is enforced where (said honestly)
217
238
 
218
- | enforced by Stellar | enforced by `policy.ts` |
239
+ | enforced by Stellar | enforced by the desk (`policy.ts`) |
219
240
  |---|---|
220
- | max per payment, payee, token, time window, one-shot execution | payments per day, running daily total, seller allow/deny list |
241
+ | payee, token, time window, one-shot execution, per-payment ceiling as a signer rule | per-payment ceiling and daily total at the approve button and again at submission, running daily total, seller allow/deny list |
221
242
 
222
243
  The vault is one payment at a time; counting payments is our job. A rule break
223
244
  means we never build the transaction, so nothing reaches the ledger.
package/dist/cli.js CHANGED
@@ -14,6 +14,8 @@
14
14
  // A seller should type one command and get a price, a link, and a key to paste
15
15
  // into their bot.
16
16
  import { createInterface } from "node:readline/promises";
17
+ import { createServer } from "node:http";
18
+ import { execSync } from "node:child_process";
17
19
  import { existsSync, readFileSync, writeFileSync, chmodSync, realpathSync } from "node:fs";
18
20
  import { pathToFileURL } from "node:url";
19
21
  import { resolve } from "node:path";
@@ -33,6 +35,39 @@ const log = (...a) => console.log("•", ...a);
33
35
  /** Print a reason and stop. Declared as a function (not an arrow in a const) so
34
36
  * TypeScript's control-flow analysis actually narrows the caller after it. */
35
37
  function bad(msg) { console.error("\n" + msg + "\n"); process.exit(1); }
38
+ /**
39
+ * Ask the OS whether a port is free, and if not, who owns it. Returns null when
40
+ * the port is available. Naming the owning process is what turns "EADDRINUSE"
41
+ * into something a seller can act on.
42
+ */
43
+ function portHolder(port) {
44
+ return new Promise((resolve) => {
45
+ const probe = createServer();
46
+ probe.once("error", (e) => {
47
+ if (e.code === "EADDRINUSE")
48
+ resolve(findListener(port) ?? "another process");
49
+ else
50
+ resolve(null); // some other bind problem; let the real listen report it
51
+ });
52
+ probe.once("listening", () => { probe.close(() => resolve(null)); });
53
+ probe.listen(port, "127.0.0.1");
54
+ });
55
+ }
56
+ /** Best-effort: which command is holding this port, from `lsof`. */
57
+ function findListener(port) {
58
+ try {
59
+ const out = execSync(`lsof -nP -ti tcp:${port} -sTCP:LISTEN 2>/dev/null`, { encoding: "utf8" });
60
+ const pid = out.trim().split("\n")[0];
61
+ if (!pid)
62
+ return null;
63
+ const cmd = execSync(`ps -o command= -p ${pid} 2>/dev/null`, { encoding: "utf8" }).trim();
64
+ const mine = cmd.includes("cli") || cmd.includes("paydlux");
65
+ return `${mine ? "another Paydlux (pid " + pid + ")" : cmd || "pid " + pid}`;
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
36
71
  /** The one file that holds everything. Refuses to read a file that is world-readable. */
37
72
  export function loadVault(path = resolve(process.cwd(), arg("file") ?? VAULT_FILE_DEFAULT)) {
38
73
  if (!existsSync(path)) {
@@ -219,6 +254,19 @@ async function sell() {
219
254
  const publicHost = tls ? PUBLIC_HOST : (arg("host") ?? undefined);
220
255
  const deskPort = Number(arg("desk") ?? 4022);
221
256
  const gwPort = Number(arg("port") ?? 4021);
257
+ // A taken port used to surface as an unhandled EADDRINUSE stack trace from
258
+ // inside Node. To someone following the README that reads as a broken
259
+ // install, and the stack names neither port nor remedy.
260
+ for (const [label, port] of [["gateway", gwPort], ["approval desk", deskPort]]) {
261
+ const who = await portHolder(port);
262
+ if (who) {
263
+ // Suggest the pair, not the clashing port twice: --port and --desk with
264
+ // the same number would fail on the next line.
265
+ bad(`port ${port} (${label}) is already in use by ${who}.\n` +
266
+ ` another Paydlux is probably running. Stop it, or move this one:\n` +
267
+ ` paydlux sell --port ${gwPort + 100} --desk ${deskPort + 100}`);
268
+ }
269
+ }
222
270
  const gw = new Gateway({
223
271
  vault, network, payee,
224
272
  deskPort,
@@ -564,8 +612,12 @@ const HELP = `paydlux — get paid by AI agents, with a human in the loop.
564
612
 
565
613
  paydlux setup [--mainnet] [--token USDC] [--fund-seed S…]
566
614
  create a vault. testnet by default (free, no wallet needed).
567
- paydlux sell [--price 1] [--route /api/data] [--split] [--tls] [--preauth]
615
+ paydlux sell [--price 1] [--route /api/data] [--max N] [--daily N]
616
+ [--split] [--tls] [--preauth]
568
617
  start the gateway and the approval desk.
618
+ --max / --daily are your spending mandate: no single payment above --max,
619
+ and nothing past --daily in a day. The desk refuses at the approve button,
620
+ so a runaway agent cannot drain the vault even if you approve in error.
569
621
  paydlux status
570
622
  balances, signers, live mandates, ledger rent.
571
623
  paydlux approver [--forget]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paydlux/agent",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Payments for AI agents that a human approves. Spending mandates enforced on the Stellar ledger: 2-of-2 vault, pre-authorised transaction envelopes, HTTP 402, USDC.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",