@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 +9 -1
- package/dist/cli.js +24 -1
- package/dist/policy.d.ts +9 -0
- package/dist/policy.js +17 -0
- package/dist/vault.d.ts +12 -0
- package/dist/vault.js +45 -0
- package/package.json +1 -1
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
|
|
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.
|
|
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",
|