@paydlux/agent 0.1.17 → 0.1.18

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
@@ -234,11 +234,19 @@ combination blocks every payment and looks like a bug in the desk.
234
234
  Verified on the published package: 6 x 4.50 against `--daily 20` -> four
235
235
  settled, two refused, vault balance moved exactly 18.00 and stopped.
236
236
 
237
+ **The budget survives a restart.** `paydlux sell` asks the ledger what the vault
238
+ already paid that payee today and starts from that number, so `ctrl-c` and
239
+ `npm run app` again does not hand the day back. If the day is already spent it
240
+ refuses to start and says so; if the ledger cannot be read it refuses to start
241
+ too, because a budget that silently reads as unspent is worse than no budget.
242
+ The banner shows what is left: `policy: max 10 per payment, 50 per day (27.50
243
+ left today)`.
244
+
237
245
  ### What is enforced where (said honestly)
238
246
 
239
247
  | enforced by Stellar | enforced by the desk (`policy.ts`) |
240
248
  |---|---|
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 |
249
+ | 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 **seeded from the ledger's own payment history**, seller allow/deny list |
242
250
 
243
251
  The vault is one payment at a time; counting payments is our job. A rule break
244
252
  means we never build the transaction, so nothing reaches the ledger.
package/dist/cli.js CHANGED
@@ -227,6 +227,29 @@ async function sell() {
227
227
  // payment with no valid payee is a payment that fails an hour later, after
228
228
  // the human already said yes.
229
229
  const payee = await ensurePayee(path, data, vault);
230
+ // The daily budget has to survive a restart. Counting only what this process
231
+ // has seen means `ctrl-c`, `npm run app` again, and the day's total is back to
232
+ // zero — so the mandate in the banner is a promise the code does not keep.
233
+ // Ask the ledger what the vault already paid this payee today and start from
234
+ // there. If the query fails, refuse to start rather than run with a budget
235
+ // that silently reads as unspent.
236
+ let alreadyPaid = 0;
237
+ try {
238
+ alreadyPaid = Number(await vault.paidToday(payee));
239
+ }
240
+ catch (e) {
241
+ bad(`cannot read today's spending from the ledger (${e?.message ?? e}).\n `
242
+ + `the daily budget of ${daily} cannot be enforced without it, so nothing was started. `
243
+ + `Check the network and try again.`);
244
+ }
245
+ policy.seedFromLedger(alreadyPaid);
246
+ if (alreadyPaid > 0) {
247
+ log(`ledger says ${alreadyPaid.toFixed(2)} of the ${daily} daily budget is already spent today`);
248
+ if (alreadyPaid >= daily) {
249
+ bad(`today's budget is already gone: ${alreadyPaid.toFixed(2)} paid, ${daily} allowed.\n `
250
+ + `the mandate would refuse every payment. Raise --daily or wait for the UTC day to roll over.`);
251
+ }
252
+ }
230
253
  if (network === "public") {
231
254
  console.log(`
232
255
  MAINNET — real money.
@@ -316,7 +339,7 @@ async function sell() {
316
339
  ${split
317
340
  ? "approvals need your phone. this machine holds one of the two keys."
318
341
  : "WARNING: this process holds BOTH human keys. Anyone with this machine can approve. Use --split for anything real."}
319
- policy: max ${maxPer} per payment, ${daily} per day
342
+ policy: max ${maxPer} per payment, ${daily} per day${alreadyPaid > 0 ? ` (${(daily - alreadyPaid).toFixed(2)} left today)` : ""}
320
343
  ctrl-c to stop
321
344
  `);
322
345
  // The desk is a queue, not a database. If it dies with a payment parked, the
package/dist/policy.d.ts CHANGED
@@ -36,6 +36,15 @@ export declare class PolicyEngine {
36
36
  policy: Policy;
37
37
  constructor(policy?: Partial<Policy>);
38
38
  record(e: SpendEvent): void;
39
+ /**
40
+ * Seed the day's total from the ledger instead of from this process's memory.
41
+ *
42
+ * `events` starts empty, so a fresh seller process believes it has spent
43
+ * nothing today. The mandate printed in the banner ("20 per day") then only
44
+ * holds until the next restart. Seeding from the ledger makes the budget a
45
+ * property of the vault, not of the uptime of whichever machine ran `sell`.
46
+ */
47
+ seedFromLedger(amount: number): void;
39
48
  /** Everything spent in the current UTC day. */
40
49
  spentToday(): number;
41
50
  today(): SpendEvent[];
package/dist/policy.js CHANGED
@@ -13,6 +13,23 @@ export class PolicyEngine {
13
13
  this.policy = { ...DEFAULT_POLICY, ...policy };
14
14
  }
15
15
  record(e) { this.events.push(e); }
16
+ /**
17
+ * Seed the day's total from the ledger instead of from this process's memory.
18
+ *
19
+ * `events` starts empty, so a fresh seller process believes it has spent
20
+ * nothing today. The mandate printed in the banner ("20 per day") then only
21
+ * holds until the next restart. Seeding from the ledger makes the budget a
22
+ * property of the vault, not of the uptime of whichever machine ran `sell`.
23
+ */
24
+ seedFromLedger(amount) {
25
+ if (!(amount > 0))
26
+ return;
27
+ // One synthetic event at the start of the day: it counts toward the total
28
+ // without pretending to be a payment this process can re-enact.
29
+ const start = new Date();
30
+ start.setUTCHours(0, 0, 0, 0);
31
+ this.record({ at: start.getTime(), amount, payee: "ledger" });
32
+ }
16
33
  /** Everything spent in the current UTC day. */
17
34
  spentToday() {
18
35
  const start = new Date();
package/dist/vault.d.ts CHANGED
@@ -103,6 +103,18 @@ export declare class StellarVault {
103
103
  */
104
104
  txExists(hash: string): Promise<boolean>;
105
105
  balance(account?: string): Promise<Amount>;
106
+ /**
107
+ * What this vault already paid `payee` in the current UTC day, according to
108
+ * the ledger — not according to this process's memory.
109
+ *
110
+ * Why this exists: the daily budget was counted in a `PolicyEngine` array that
111
+ * lives only in RAM. Restart the seller and the counter resets to zero, so the
112
+ * mandate the banner advertises is only as strong as the process uptime. A
113
+ * seller with `--daily 20` who restarts twice can pay out 60. The ledger
114
+ * already recorded every payment that went through, so the honest number is a
115
+ * query away, and it survives restarts, crashes and redeploys.
116
+ */
117
+ paidToday(payee: string): Promise<Amount>;
106
118
  /**
107
119
  * Build a mandate and register it on the vault.
108
120
  *
package/dist/vault.js CHANGED
@@ -191,6 +191,51 @@ export class StellarVault {
191
191
  return b?.balance ?? "0";
192
192
  }
193
193
  // --------------------------------------------------------------- mandates
194
+ /**
195
+ * What this vault already paid `payee` in the current UTC day, according to
196
+ * the ledger — not according to this process's memory.
197
+ *
198
+ * Why this exists: the daily budget was counted in a `PolicyEngine` array that
199
+ * lives only in RAM. Restart the seller and the counter resets to zero, so the
200
+ * mandate the banner advertises is only as strong as the process uptime. A
201
+ * seller with `--daily 20` who restarts twice can pay out 60. The ledger
202
+ * already recorded every payment that went through, so the honest number is a
203
+ * query away, and it survives restarts, crashes and redeploys.
204
+ */
205
+ async paidToday(payee) {
206
+ const start = new Date();
207
+ start.setUTCHours(0, 0, 0, 0);
208
+ let total = 0;
209
+ let cursor;
210
+ // Payments are walked backwards from newest, stopping at the UTC midnight
211
+ // boundary, so the cost is proportional to today's traffic and not to the
212
+ // account's whole history.
213
+ for (let page = 0; page < 10; page++) {
214
+ const url = `${HORIZON_URL[this.network]}/accounts/${this.address}/payments`
215
+ + `?order=desc&limit=200${cursor ? `&cursor=${cursor}` : ""}`;
216
+ const res = await fetch(url);
217
+ if (!res.ok)
218
+ throw new Error(`ledger payments query failed: ${res.status}`);
219
+ const recs = (await res.json())?._embedded?.records ?? [];
220
+ if (!recs.length)
221
+ break;
222
+ for (const e of recs) {
223
+ if (new Date(e.created_at).getTime() < start.getTime())
224
+ return total.toFixed(7);
225
+ if (e.type !== "payment" && e.type !== "claimable_balance_claim")
226
+ continue;
227
+ if (e.to !== payee)
228
+ continue;
229
+ if (e.asset_type === "native")
230
+ continue;
231
+ if (e.asset_code !== this.token.getCode() || e.asset_issuer !== this.token.getIssuer())
232
+ continue;
233
+ total += Number(e.amount);
234
+ }
235
+ cursor = recs[recs.length - 1].paging_token;
236
+ }
237
+ return total.toFixed(7);
238
+ }
194
239
  /**
195
240
  * Build a mandate and register it on the vault.
196
241
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paydlux/agent",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
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",