@tokagent/tokagentos 2.0.13 → 2.0.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokagent/tokagentos",
3
- "version": "2.0.13",
3
+ "version": "2.0.15",
4
4
  "description": "tokagentOS CLI - Create and upgrade tokagentOS project templates",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokagent/plugin-tokagent-billing",
3
- "version": "2.0.6",
3
+ "version": "2.0.8",
4
4
  "description": "elizaOS plugin: Web3 credit-billing routes and middleware for the tokagentos LLM gateway.",
5
5
  "type": "module",
6
6
  "publishConfig": { "access": "public" },
@@ -716,10 +716,15 @@ function renderTopBar() {
716
716
 
717
717
  function renderKpis() {
718
718
  const c = state.credits;
719
- const balance = c?.ledger?.balance ?? c?.onChainCredits;
719
+ // Server returns BOTH nested (c.ledger.balance) and flat (c.balance).
720
+ // Prefer nested for clients that expect the structured shape, fall back
721
+ // to flat for forward/backward compat with the simpler API surface.
722
+ const balance = c?.ledger?.balance ?? c?.balance ?? c?.onChainCredits;
723
+ const reserved = c?.ledger?.reserved ?? c?.reserved ?? 0n;
724
+ const accrued = c?.ledger?.accrued ?? c?.accrued ?? 0n;
720
725
  $("#kpi-balance").textContent = fmtPton(balance);
721
- $("#kpi-reserved").textContent = fmtPton(c?.ledger?.reserved ?? 0n) + " PTON";
722
- $("#kpi-accrued").textContent = fmtPton(c?.ledger?.accrued ?? 0n) + " PTON";
726
+ $("#kpi-reserved").textContent = fmtPton(reserved) + " PTON";
727
+ $("#kpi-accrued").textContent = fmtPton(accrued) + " PTON";
723
728
  $("#kpi-balance-usd").textContent = fmtUsdFromAttoPton(balance, state.tonUsd);
724
729
  // Wallet holdings (outside the vault). ETH and PTON share 18 decimals so
725
730
  // fmtPton works for both — only the unit label differs.
@@ -19,7 +19,7 @@ import {
19
19
  } from "../state.js";
20
20
  import { resolveBillingIdentity } from "../middleware/api-key-resolve.js";
21
21
  import { pickForward, forward, ensureClientReady } from "../lib/forward.js";
22
- import { creditState } from "@tokagentos/billing";
22
+ import { creditState, hydrate as hydrateCredits, readCredits } from "@tokagentos/billing";
23
23
  import { eq } from "drizzle-orm";
24
24
 
25
25
  // ---------------------------------------------------------------------------
@@ -63,7 +63,7 @@ async function handleGetCreditsMe(
63
63
  _runtime: IAgentRuntime,
64
64
  ): Promise<void> {
65
65
  if (!isBillingStateInitialized()) return billingUnavailable(res);
66
- const { db, config } = getServerBillingState();
66
+ const { db, config, clients } = getServerBillingState();
67
67
  if (!config.enabled) return billingUnavailable(res);
68
68
 
69
69
  const identity = await resolveBillingIdentity(toIncomingMessage(req));
@@ -75,6 +75,35 @@ async function handleGetCreditsMe(
75
75
  const wallet: Address = identity.wallet;
76
76
  const walletKey = wallet.toLowerCase();
77
77
 
78
+ // Sync on-chain credits → DB ledger BEFORE returning the balance.
79
+ //
80
+ // Why hydrate-on-read instead of a separate deposit watcher service:
81
+ // - depositX402 is now PERMISSIONLESS (any wallet can submit a user's
82
+ // signed EIP-3009 auth) — handleTopupSettle is only ONE possible
83
+ // submitter, so we can't rely on the settle path to credit the DB.
84
+ // - A dedicated event listener could miss events during agent
85
+ // downtime or RPC outages; hydrate-on-read self-heals at request
86
+ // time.
87
+ // - The vault's `credits[user]` mapping is the source of truth.
88
+ // hydrate() reconciles: balance = onChain - (reserved + accrued).
89
+ //
90
+ // Costs: one eth_call per dashboard refresh. Acceptable — this route is
91
+ // not on the hot inference path.
92
+ //
93
+ // Failure handling: if the RPC call throws, fall back to the stale DB
94
+ // row rather than 500ing. The user sees a slightly old balance instead
95
+ // of a broken page.
96
+ try {
97
+ const onChainCredits = await readCredits(
98
+ clients,
99
+ config.vaultAddress,
100
+ wallet,
101
+ );
102
+ await hydrateCredits(db, wallet, onChainCredits);
103
+ } catch (_err) {
104
+ // Swallow — fall back to whatever the DB has. Logged at hydrate level.
105
+ }
106
+
78
107
  // Read the credit state row (may not exist for a new wallet).
79
108
  const rows = await db
80
109
  .select()
@@ -83,11 +112,19 @@ async function handleGetCreditsMe(
83
112
 
84
113
  const row = rows[0];
85
114
 
115
+ // Response shape: both flat (legacy clients) and nested under `ledger`
116
+ // (the dashboard SPA which reads `c.ledger.balance`). Keeping both keys
117
+ // is cheap and avoids a contract break for any external caller already
118
+ // depending on the flat shape.
119
+ const balance = row ? row.balance.toString() : "0";
120
+ const reserved = row ? row.reserved.toString() : "0";
121
+ const accrued = row ? row.accrued.toString() : "0";
86
122
  res.status(200).json({
87
123
  wallet,
88
- balance: row ? row.balance.toString() : "0",
89
- reserved: row ? row.reserved.toString() : "0",
90
- accrued: row ? row.accrued.toString() : "0",
124
+ balance,
125
+ reserved,
126
+ accrued,
127
+ ledger: { balance, reserved, accrued },
91
128
  });
92
129
  }
93
130
 
@@ -263,7 +263,7 @@ async function handleCountTokens(
263
263
  * or `{ "available": false }` when no price is cached.
264
264
  */
265
265
  async function handlePrice(
266
- req: RouteRequest,
266
+ _req: RouteRequest,
267
267
  res: RouteResponse,
268
268
  _runtime: IAgentRuntime,
269
269
  ): Promise<void> {
@@ -271,11 +271,9 @@ async function handlePrice(
271
271
  const { config, twapCache } = getBillingState();
272
272
  if (!config.enabled) return billingUnavailable(res);
273
273
 
274
- const identity = await resolveBillingIdentity(toIncomingMessage(req));
275
- if (!identity) {
276
- res.status(401).json({ error: "Authentication required." });
277
- return;
278
- }
274
+ // TON/USD is a public oracle read — no identity required. The dashboard
275
+ // KPI shows this rate before the user has signed in, so gating it on
276
+ // SIWE auth would leave the price displayed as "—" until login completes.
279
277
 
280
278
  // 1. Priority override — operator-pinned price freeze.
281
279
  // `BILLING_FIXED_TON_USD` is env-only (not exposed in the setup wizard),
@@ -194,26 +194,53 @@ async function handleTopupQuote(
194
194
  return;
195
195
  }
196
196
 
197
- // Parse optional amountUsd from body; fall back to the configured default.
197
+ // Parse optional amountPton OR amountUsd from body; fall back to default.
198
+ // Prefer amountPton when both are present — it's the exact value the user
199
+ // signs in the EIP-3009 authorization, and settlement strictly equality-
200
+ // checks signature.value === quote.amountPton. Routing the user's intent
201
+ // through a USD round-trip would round and mismatch the signature.
198
202
  const body = req.body as Record<string, unknown> | undefined;
203
+ let amountPton: bigint;
199
204
  let amountUsd: number;
200
- if (body?.["amountUsd"] !== undefined) {
205
+ if (body?.["amountPton"] !== undefined) {
206
+ const raw = body["amountPton"];
207
+ let parsed: bigint;
208
+ try {
209
+ // Accept either a decimal string (atto-units) or a JS number for
210
+ // backward compat with older clients.
211
+ if (typeof raw === "string") parsed = BigInt(raw);
212
+ else if (typeof raw === "number" && Number.isFinite(raw) && raw > 0)
213
+ parsed = BigInt(Math.floor(raw));
214
+ else throw new Error("amountPton must be a positive decimal string or number (atto-units)");
215
+ } catch (e) {
216
+ res.status(400).json({
217
+ error: e instanceof Error ? e.message : "Invalid amountPton",
218
+ });
219
+ return;
220
+ }
221
+ if (parsed <= 0n) {
222
+ res.status(400).json({ error: "amountPton must be > 0" });
223
+ return;
224
+ }
225
+ amountPton = parsed;
226
+ // Derive USD for the response envelope. Settlement only checks PTON.
227
+ amountUsd = (Number(amountPton) / 1e18) * tonUsd;
228
+ } else if (body?.["amountUsd"] !== undefined) {
201
229
  const raw = body["amountUsd"];
202
230
  if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0) {
203
231
  res.status(400).json({ error: "amountUsd must be a positive finite number." });
204
232
  return;
205
233
  }
206
234
  amountUsd = raw;
235
+ amountPton = usdToPton(amountUsd, tonUsd);
207
236
  } else {
208
- // Convert default PTON amount → USD.
209
- const defaultPton = config.topupAmountPton;
210
- // amountUsd = ptonAmount * tonUsd / 1e18
211
- amountUsd = Number(defaultPton) * tonUsd / 1e18;
237
+ // Neither provided — use configured default PTON amount.
238
+ amountPton = config.topupAmountPton;
239
+ amountUsd = Number(amountPton) * tonUsd / 1e18;
212
240
  }
213
241
 
214
- const amountPton = usdToPton(amountUsd, tonUsd);
215
242
  if (amountPton <= 0n) {
216
- res.status(400).json({ error: "Computed PTON amount is zero — check amountUsd." });
243
+ res.status(400).json({ error: "Computed PTON amount is zero — check amount." });
217
244
  return;
218
245
  }
219
246
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "1.0.0",
3
- "generatedAt": "2026-05-19T14:16:04.436Z",
3
+ "generatedAt": "2026-05-19T17:46:05.725Z",
4
4
  "repoUrl": "https://github.com/elizaos/eliza",
5
5
  "templates": [
6
6
  {