thaifi-wallet-cli 0.3.6 → 0.5.0

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
@@ -44,6 +44,8 @@ Set `THAIFI_WALLET_URL` / `THAIFI_RPC_URL` to target other deployments.
44
44
  | `tokens` | List supported TIP-20 tokens (symbol, decimals, address) |
45
45
  | `balance [--token <symbol\|addr>]` | Balances (default: all supported tokens) |
46
46
  | `transfer <to> <amount> [--token <symbol\|addr>]` | Send tokens (default: pathUSD; limit enforced on-chain) |
47
+ | `fund [--no-browser]` | Show the deposit address and open the web Deposit page |
48
+ | `deposit --amount <THB> [--token THCFI\|THCOC] [--ref <id>] [--no-open] [--json]` | **PromptPay QR top-up (1 THB = 1 token)**: order a QR, show it for a human to scan, poll until the tokens are minted |
47
49
  | `request <url> [-X -H -d --json --dry-run --max-spend]` | HTTP with automatic x402/MPP payment **and ThaiFiAgent identity auth (401/403 answered by signing the challenge)** |
48
50
  | `services [--search q]` | Discover services from the MPP registry (`THAIFI_MPP_REGISTRY`) |
49
51
  | `logout` | Remove the local key (revoke on-chain from the web wallet) |
@@ -51,6 +53,38 @@ Set `THAIFI_WALLET_URL` / `THAIFI_RPC_URL` to target other deployments.
51
53
  `--token` accepts a symbol (case-insensitive) or a contract address; unknown
52
54
  addresses are treated as custom TIP-20s (6 decimals).
53
55
 
56
+ ## Deposit (PromptPay top-up)
57
+
58
+ `thaifi deposit --amount 50` orders a PromptPay QR for the paired wallet and
59
+ waits until the tokens are minted (1 THB = 1 token). There is **no API key**:
60
+ the backend verifies on-chain (AccountKeychain precompile, TIP-1011) that the
61
+ agent's access key belongs to a wallet and credits THAT wallet — the recipient
62
+ never comes from the CLI.
63
+
64
+ ```
65
+ $ thaifi deposit --amount 50
66
+ PromptPay deposit — 50.00 THB -> THCFI (1 THB = 1 token)
67
+ Credited wallet: 0x…
68
+ <terminal QR block>
69
+ QR_FILE=~/.thaifi/qr-<referenceNo>.png
70
+ REFERENCE=<referenceNo>
71
+ AMOUNT=50.00 THB -> THCFI
72
+ RESUME=thaifi deposit --ref <referenceNo>
73
+ STATUS=pending (scan the QR to pay)
74
+ STATUS=delivered
75
+ TX=0x… Explorer: https://exp.thaifi.com/tx/0x…
76
+ ```
77
+
78
+ - The QR is saved to `~/.thaifi/qr-<referenceNo>.png` (chmod 600), rendered as
79
+ terminal blocks, and opened in the default viewer (disable with `--no-open`).
80
+ - Orders expire after 15 minutes; paying late still delivers. Resume watching
81
+ any order with `--ref <referenceNo>` — the status response includes the QR
82
+ again, so a restarted CLI can re-display it.
83
+ - Limits: 6–10,000 THB per order, 50,000 THB per wallet per 24h, 5 pending
84
+ orders per wallet. `--json` prints machine-readable output.
85
+ - Requires an approved pairing (`thaifi login`); a revoked/expired key fails
86
+ with 401 on-chain verification.
87
+
54
88
  ## Security notes
55
89
 
56
90
  - The recovery password / passphrase is never involved here — the agent key is
package/dist/agentAuth.js CHANGED
@@ -18,6 +18,31 @@ const ZERO_BODY_HASH = `0x${"00".repeat(32)}`;
18
18
  function b64uDecodeBytes(value) {
19
19
  return new Uint8Array(Buffer.from(value.replace(/-/g, "+").replace(/_/g, "/"), "base64"));
20
20
  }
21
+ /** base64url-encode (utf8) — used for the ThaiFiAgent credential header. */
22
+ export function b64uEncode(s) {
23
+ return Buffer.from(s, "utf8")
24
+ .toString("base64")
25
+ .replace(/\+/g, "-")
26
+ .replace(/\//g, "_")
27
+ .replace(/=+$/, "");
28
+ }
29
+ /** Parse a `WWW-Authenticate: ThaiFiAgent realm="…", challenge="<b64u>"`
30
+ * header — ThaiFi Agent Auth (identity proof, no payment). */
31
+ export function parseAgentChallenge(header) {
32
+ const m = /^ThaiFiAgent\s+(.+)$/i.exec(header.trim());
33
+ if (!m)
34
+ return null;
35
+ const params = {};
36
+ const re = /([a-zA-Z]+)=((?:"((?:[^"\\]|\\.)*)")|[^\s,]+)/g;
37
+ let pm;
38
+ while ((pm = re.exec(m[1]))) {
39
+ const value = pm[3] !== undefined ? pm[3].replace(/\\(.)/g, "$1") : pm[2];
40
+ params[pm[1].toLowerCase()] = value;
41
+ }
42
+ if (!params.realm || !params.challenge)
43
+ return null;
44
+ return { realm: params.realm, nonce: params.challenge };
45
+ }
21
46
  export function agentAuthHash(params) {
22
47
  const bodyHash = params.body && params.body.length > 0 ? sha256(stringToBytes(params.body)) : ZERO_BODY_HASH;
23
48
  return keccak256(concatHex([
package/dist/chain.js CHANGED
@@ -3,14 +3,14 @@ import { http, defineChain } from "viem";
3
3
  import { createClient, tempoActions } from "viem/tempo";
4
4
  import { Actions, Account, Addresses, Chain as TempoChains } from "viem/tempo";
5
5
  import { CONFIG } from "./config.js";
6
- import { PATHUSD, TOKENS } from "./tokens.js";
6
+ import { PATHTHB, TOKENS } from "./tokens.js";
7
7
  /** ThaiFi chain 17 — clone of the tempo chain definition (formatters/serializers) with our ids. */
8
8
  export const thaiFi = defineChain({
9
9
  ...TempoChains.mainnet,
10
10
  id: CONFIG.chainId,
11
11
  name: "ThaiFi",
12
12
  network: "thaifi",
13
- nativeCurrency: { name: "pathUSD", symbol: "pathUSD", decimals: 6 },
13
+ nativeCurrency: { name: "pathTHB", symbol: "pathTHB", decimals: 6 },
14
14
  rpcUrls: { default: { http: [CONFIG.rpcUrl] } },
15
15
  blockExplorers: {
16
16
  default: { name: "ThaiFi Explorer", url: "https://exp.thaifi.com" },
@@ -34,8 +34,8 @@ export function createAgentClient(store, feeToken) {
34
34
  /** Min balance that makes a token usable as gas (~0.01; gas ≈ 0.003/tx). */
35
35
  const GAS_MIN_UNITS = 10000n;
36
36
  /**
37
- * Pick the gas token by balance: pathUSD → THCFI → THCOC. The chain swaps the
38
- * chosen token to the validator's pathUSD through the protocol FeeAMM.
37
+ * Pick the gas token by balance: pathTHB → THCFI → THCOC. The chain swaps the
38
+ * chosen token to the validator's pathTHB through the protocol FeeAMM.
39
39
  */
40
40
  export async function pickFeeToken(client, userAddress) {
41
41
  for (const t of TOKENS) {
@@ -43,7 +43,7 @@ export async function pickFeeToken(client, userAddress) {
43
43
  if (bal >= GAS_MIN_UNITS)
44
44
  return t;
45
45
  }
46
- return PATHUSD;
46
+ return PATHTHB;
47
47
  }
48
48
  export async function tokenBalance(client, token, address) {
49
49
  return client.readContract({
@@ -0,0 +1,213 @@
1
+ /** `thaifi deposit` — PromptPay QR top-up (1 THB = 1 token).
2
+ *
3
+ * Creates a QR order on wallet.thaifi.com and waits until the tokens are
4
+ * minted. Auth is ThaiFi Agent Auth (no API key): the backend verifies
5
+ * on-chain (AccountKeychain precompile) that this agent's access key belongs
6
+ * to a wallet and credits THAT wallet — the CLI never sends a recipient.
7
+ */
8
+ import { mkdirSync, writeFileSync } from "node:fs";
9
+ import { dirname, join } from "node:path";
10
+ import png from "pngjs";
11
+ import * as jsqrModule from "jsqr";
12
+ import qrcodeTerminal from "qrcode-terminal";
13
+ import { loadStore, storePath } from "../store.js";
14
+ import { CONFIG } from "../config.js";
15
+ import { b64uEncode, buildAgentAuthCredential, parseAgentChallenge, } from "../agentAuth.js";
16
+ import { openBrowser } from "./fund.js";
17
+ const ORDER_PATH = "/api/agent/deposit/qr";
18
+ const MIN_THB = 6;
19
+ const MAX_THB = 10000;
20
+ /** Orders expire after 15 min, but a late payment still delivers — keep
21
+ * watching that long past expiry. */
22
+ const LATE_GRACE_MS = 15 * 60 * 1000;
23
+ const TERMINAL_STATUSES = new Set(["delivered", "cancelled", "capped", "qr_error"]);
24
+ const { PNG } = png;
25
+ // NodeNext misbinds jsqr's CJS `export default` to the whole module namespace
26
+ // when importing from ESM; at runtime the namespace's `.default` IS the
27
+ // function (verified) — pick it with an explicit cast.
28
+ const jsQR = jsqrModule.default;
29
+ const STATUS_HINTS = {
30
+ pending: "scan the QR to pay",
31
+ paid: "payment received — minting",
32
+ delivering: "minting tokens",
33
+ delivered: "tokens are in the wallet",
34
+ cancelled: "order was cancelled",
35
+ capped: "over the daily cap — needs operator review",
36
+ qr_error: "QR creation failed",
37
+ };
38
+ // ---------------------------------------------------------------- HTTP + TAA
39
+ /** fetch with automatic ThaiFi Agent Auth: on a 401/403 challenge, sign with
40
+ * the agent access key (identity proof, no payment) and retry once. The
41
+ * server verifies the key against the AccountKeychain precompile. */
42
+ async function agentFetch(store, url, init) {
43
+ const headers = {};
44
+ if (init.body !== undefined)
45
+ headers["Content-Type"] = "application/json";
46
+ let res = await fetch(url, { method: init.method, headers, body: init.body });
47
+ if (res.status === 401 || res.status === 403) {
48
+ const challenge = parseAgentChallenge(res.headers.get("www-authenticate") ?? "");
49
+ if (challenge) {
50
+ const credential = await buildAgentAuthCredential(store, challenge, {
51
+ method: init.method,
52
+ url,
53
+ body: init.body,
54
+ });
55
+ res = await fetch(url, {
56
+ method: init.method,
57
+ headers: {
58
+ ...headers,
59
+ Authorization: "ThaiFiAgent " + b64uEncode(JSON.stringify(credential)),
60
+ },
61
+ body: init.body,
62
+ });
63
+ }
64
+ }
65
+ return res;
66
+ }
67
+ // ------------------------------------------------------------------ QR output
68
+ /** Decode the payso QR PNG and re-render it as terminal blocks so a human
69
+ * watching the session can scan straight off the screen. Best-effort. */
70
+ function renderTerminalQr(dataUrl) {
71
+ try {
72
+ const b64 = dataUrl.slice(dataUrl.indexOf(",") + 1);
73
+ const img = PNG.sync.read(Buffer.from(b64, "base64"));
74
+ const decoded = jsQR(new Uint8ClampedArray(img.data), img.width, img.height);
75
+ if (!decoded?.data)
76
+ return;
77
+ console.log("");
78
+ qrcodeTerminal.generate(decoded.data, { small: true });
79
+ console.log("");
80
+ }
81
+ catch {
82
+ // terminal rendering is a convenience — the saved PNG always works
83
+ }
84
+ }
85
+ /** Save the QR image to ~/.thaifi/qr-<referenceNo>.<ext> (chmod 600) so the
86
+ * agent has a durable path to show the user. Returns the path or null. */
87
+ function saveQrImage(dataUrl, referenceNo) {
88
+ try {
89
+ const m = /^data:image\/(png|jpe?g|webp);base64,(.+)$/s.exec(dataUrl);
90
+ if (!m)
91
+ return null;
92
+ const ext = m[1] === "jpeg" ? "jpg" : m[1];
93
+ const dir = dirname(storePath);
94
+ const file = join(dir, `qr-${referenceNo}.${ext}`);
95
+ mkdirSync(dir, { recursive: true });
96
+ writeFileSync(file, Buffer.from(m[2], "base64"), { mode: 0o600 });
97
+ return file;
98
+ }
99
+ catch {
100
+ return null;
101
+ }
102
+ }
103
+ // --------------------------------------------------------------------- polling
104
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
105
+ /** Poll the order until a terminal status (or the late-payment grace runs
106
+ * out). Prints STATUS=/TX= lines — machine-parsable like `PAIR_URL=`. */
107
+ async function watchOrder(store, orderUrl, order, options) {
108
+ const statusUrl = `${orderUrl}/${order.referenceNo}`;
109
+ const deadline = (order.expiresAt || Date.now()) + LATE_GRACE_MS;
110
+ let last = "";
111
+ while (Date.now() < deadline) {
112
+ const res = await agentFetch(store, statusUrl, { method: "GET" });
113
+ const data = (await res.json().catch(() => null));
114
+ if (res.ok && data?.status && data.status !== last) {
115
+ last = data.status;
116
+ if (options.json) {
117
+ const { image, ...rest } = data;
118
+ console.log(JSON.stringify(rest, null, 2));
119
+ }
120
+ else {
121
+ const hint = STATUS_HINTS[data.status];
122
+ console.log(`STATUS=${data.status}${hint ? ` (${hint})` : ""}`);
123
+ }
124
+ if (data.status === "delivered" && data.txHash && !options.json) {
125
+ console.log(`TX=${data.txHash}`);
126
+ console.log(`Explorer: https://exp.thaifi.com/tx/${data.txHash}`);
127
+ }
128
+ if (TERMINAL_STATUSES.has(data.status)) {
129
+ if (data.status !== "delivered")
130
+ process.exitCode = 1;
131
+ return;
132
+ }
133
+ }
134
+ await sleep(CONFIG.pollIntervalMs);
135
+ }
136
+ console.error("Timed out waiting for payment. Payment after expiry still delivers — resume later with:");
137
+ console.error(` thaifi deposit --ref ${order.referenceNo}`);
138
+ process.exitCode = 1;
139
+ }
140
+ export async function deposit(options = {}) {
141
+ const store = loadStore();
142
+ if (!store?.userAddress) {
143
+ console.error("Not paired. Run `thaifi login` first.");
144
+ process.exitCode = 1;
145
+ return;
146
+ }
147
+ // CONFIG (env THAIFI_WALLET_URL > default) — same precedence as `login`,
148
+ // NOT store.walletUrl: a stale value would silently hit the wrong host.
149
+ const base = (process.env.THAIFI_WALLET_URL || CONFIG.walletUrl).replace(/\/+$/, "");
150
+ const orderUrl = base + ORDER_PATH;
151
+ // --ref: watch an existing order (e.g. after a restart) instead of creating one.
152
+ if (options.ref) {
153
+ const res = await agentFetch(store, `${orderUrl}/${options.ref.trim()}`, { method: "GET" });
154
+ const data = (await res.json().catch(() => null));
155
+ if (!res.ok || !data?.referenceNo) {
156
+ console.error(`${res.status}: ${data?.error ?? res.statusText}`);
157
+ process.exitCode = 1;
158
+ return;
159
+ }
160
+ if (!options.json) {
161
+ console.log(`REFERENCE=${data.referenceNo}`);
162
+ console.log(`AMOUNT=${data.total} THB -> ${data.token ?? "?"}`);
163
+ }
164
+ await watchOrder(store, orderUrl, data, options);
165
+ return;
166
+ }
167
+ const token = (options.token ?? "THCFI").toUpperCase();
168
+ if (token !== "THCFI" && token !== "THCOC") {
169
+ console.error("--token must be THCFI or THCOC");
170
+ process.exitCode = 1;
171
+ return;
172
+ }
173
+ const amount = Number(options.amount);
174
+ if (!options.amount || !Number.isFinite(amount) || amount < MIN_THB || amount > MAX_THB) {
175
+ console.error(`--amount must be ${MIN_THB}-${MAX_THB} THB`);
176
+ process.exitCode = 1;
177
+ return;
178
+ }
179
+ const body = JSON.stringify({ amount, token });
180
+ const res = await agentFetch(store, orderUrl, { method: "POST", body });
181
+ const data = (await res.json().catch(() => null));
182
+ if (!res.ok || !data?.referenceNo) {
183
+ console.error(`${res.status}: ${data?.error ?? res.statusText}`);
184
+ if (data?.detail && typeof data.detail === "object") {
185
+ console.error(JSON.stringify(data.detail).slice(0, 400));
186
+ }
187
+ process.exitCode = 1;
188
+ return;
189
+ }
190
+ const file = data.image ? saveQrImage(data.image, data.referenceNo) : null;
191
+ if (options.json) {
192
+ const { image, ...rest } = data;
193
+ console.log(JSON.stringify({ ...rest, qrFile: file }, null, 2));
194
+ }
195
+ else {
196
+ console.log(`PromptPay deposit — ${data.total} THB -> ${token} (1 THB = 1 token)`);
197
+ console.log(`Credited wallet: ${store.userAddress}`);
198
+ if (data.image)
199
+ renderTerminalQr(data.image);
200
+ if (file)
201
+ console.log(`QR_FILE=${file}`);
202
+ console.log(`REFERENCE=${data.referenceNo}`);
203
+ console.log(`AMOUNT=${data.total} THB -> ${token}`);
204
+ console.log(`RESUME=thaifi deposit --ref ${data.referenceNo}`);
205
+ console.log("");
206
+ console.log("Scan the QR with any banking app — polling until the tokens are minted");
207
+ console.log("(Ctrl-C to stop; resume with the RESUME command above).");
208
+ }
209
+ if (file && options.open !== false) {
210
+ await openBrowser("file://" + file);
211
+ }
212
+ await watchOrder(store, orderUrl, data, options);
213
+ }
@@ -1,6 +1,6 @@
1
1
  /** `thaifi fund` — show the deposit address and open the web Deposit page. */
2
2
  import { loadStore } from "../store.js";
3
- async function openBrowser(url) {
3
+ export async function openBrowser(url) {
4
4
  const { exec } = await import("node:child_process");
5
5
  const cmd = process.platform === "darwin"
6
6
  ? `open "${url}"`
@@ -161,7 +161,7 @@ export async function login(options = {}) {
161
161
  console.log(" Wallet account: " + status.userAddress);
162
162
  console.log(" Spend limit: " +
163
163
  Number(status.limitAmount ?? 0) / 10 ** 6 +
164
- " pathUSD / " +
164
+ " pathTHB / " +
165
165
  Math.round((status.limitPeriod ?? 0) / 86400) +
166
166
  " days (THCFI/THCOC limits set at approval)");
167
167
  console.log(" Key expires: " + (status.expiry ? new Date(status.expiry * 1000).toISOString() : "never"));
@@ -0,0 +1,161 @@
1
+ /**
2
+ * `thaifi mcp` — stdio MCP server that exposes every paid service in the
3
+ * ThaiFi MPP catalog (mpp.thaifi.com /services) as MCP tools, paying per call
4
+ * with the agent access key already paired on this machine.
5
+ *
6
+ * Tools are generated from the live registry: `POST` services get an
7
+ * inputSchema from the catalog's `requestBody`; tool calls go through
8
+ * paidFetch, which pays 402 challenges automatically (the on-chain access-key
9
+ * spending limit still applies). THAIFI_MCP_MAX_SPEND (USD, default 5) caps
10
+ * every call on top.
11
+ *
12
+ * stdout carries only JSON-RPC — everything else goes to stderr.
13
+ */
14
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
15
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
16
+ import { z } from "zod";
17
+ import { paidFetch, fmtAmount } from "../paidFetch.js";
18
+ import { loadStore } from "../store.js";
19
+ const err = (msg) => {
20
+ console.error(msg);
21
+ };
22
+ function registryUrl() {
23
+ return (process.env.THAIFI_MPP_REGISTRY ?? "https://mpp.thaifi.com").replace(/\/+$/, "");
24
+ }
25
+ async function fetchCatalog() {
26
+ const res = await fetch(registryUrl() + "/services", {
27
+ headers: { "user-agent": "thaifi-wallet-cli-mcp" },
28
+ signal: AbortSignal.timeout(15_000),
29
+ });
30
+ if (!res.ok)
31
+ throw new Error(`Registry returned HTTP ${res.status} for GET ${registryUrl()}/services`);
32
+ const body = (await res.json());
33
+ return (body.services ?? []).filter((s) => s.auth === "mpp");
34
+ }
35
+ /** Convert a JSON-schema property to a zod shape (registerTool takes zod). */
36
+ function propToZod(prop) {
37
+ const desc = typeof prop.description === "string" ? prop.description : undefined;
38
+ switch (prop.type) {
39
+ case "string": {
40
+ let s = z.string();
41
+ if (desc)
42
+ s = s.describe(desc);
43
+ return s;
44
+ }
45
+ case "number":
46
+ case "integer": {
47
+ let n = z.number();
48
+ if (desc)
49
+ n = n.describe(desc);
50
+ return n;
51
+ }
52
+ case "boolean":
53
+ return z.boolean();
54
+ case "array":
55
+ return z.array(z.any());
56
+ default:
57
+ return z.any();
58
+ }
59
+ }
60
+ /** Build a zod shape from the catalog's requestBody JSON schema. */
61
+ function zodFromSchema(schema) {
62
+ if (!schema || schema.type !== "object" || typeof schema.properties !== "object")
63
+ return {};
64
+ const shape = {};
65
+ const required = Array.isArray(schema.required) ? schema.required : [];
66
+ for (const [key, rawProp] of Object.entries(schema.properties)) {
67
+ const zt = propToZod(rawProp);
68
+ shape[key] = required.includes(key) ? zt : zt.optional();
69
+ }
70
+ return shape;
71
+ }
72
+ /** Translate a tool call into the registry request. GET tools accept no args. */
73
+ function requestFor(svc, input) {
74
+ const url = registryUrl() + svc.path;
75
+ if (svc.method === "GET")
76
+ return { url, method: "GET" };
77
+ const bodyObj = svc.requestBody
78
+ ? input
79
+ : Object.keys(input).length === 0
80
+ ? undefined
81
+ : { body: input };
82
+ return { url, method: "POST", body: bodyObj ? JSON.stringify(bodyObj) : undefined };
83
+ }
84
+ export async function mcp() {
85
+ const store = loadStore();
86
+ if (!store?.userAddress || !store?.privateKey) {
87
+ // Not fatal for list-tools — but tell the operator loudly on stderr.
88
+ err("thaifi mcp: not paired — tools will refuse to pay until `thaifi login` completes.");
89
+ }
90
+ const maxSpend = BigInt(Math.max(1, Math.round(Number(process.env.THAIFI_MCP_MAX_SPEND || 5))) * 1_000_000);
91
+ const server = new McpServer({ name: "thaifi", version: "0.5.0" }, { capabilities: { tools: {} } });
92
+ let catalog = [];
93
+ try {
94
+ catalog = await fetchCatalog();
95
+ }
96
+ catch (e) {
97
+ err(`thaifi mcp: could not load service catalog (${e instanceof Error ? e.message : String(e)}).`);
98
+ err("Tools will be registered once the catalog is reachable — restarting the MCP server retries it.");
99
+ }
100
+ for (const svc of catalog) {
101
+ const toolName = `thaifi_${svc.id}`.replace(/[^a-zA-Z0-9_]/g, "_");
102
+ const shape = svc.method === "GET" ? {} : zodFromSchema(svc.requestBody);
103
+ server.tool(toolName, `${svc.title} — ${svc.description} [price: ${svc.price}]`, shape, async (input) => {
104
+ const req = requestFor(svc, input);
105
+ const result = await paidFetch(req.url, {
106
+ method: req.method,
107
+ body: req.body,
108
+ maxSpend,
109
+ log: (m) => err(`thaifi mcp [${toolName}]: ${m}`),
110
+ });
111
+ const content = [];
112
+ let parsed = null;
113
+ try {
114
+ parsed = JSON.parse(result.text);
115
+ }
116
+ catch { /* not JSON */ }
117
+ const b64 = parsed?.image_base64 ?? parsed?.data?.image_base64;
118
+ if (typeof b64 === "string" && b64.length > 1000) {
119
+ const mime = String(parsed?.mime_type ?? parsed?.data?.mime_type ?? "image/png");
120
+ content.push({ type: "image", data: b64, mimeType: mime });
121
+ try {
122
+ const { writeFileSync } = await import("node:fs");
123
+ const id = String(parsed?.id ?? parsed?.data?.id ?? Date.now()).toString().replace(/[^\w-]/g, "");
124
+ const ext = mime.includes("webp") ? "webp" : mime.includes("jpeg") ? "jpg" : "png";
125
+ writeFileSync(`image-${id}.${ext}`, Buffer.from(b64, "base64"));
126
+ }
127
+ catch { /* saving is best-effort */ }
128
+ }
129
+ if (result.unpaid402) {
130
+ return { isError: true, content: [{ type: "text", text: `Unpaid (402): ${result.unpaid402}\nPair this machine with \`thaifi login\` and raise THAIFI_MCP_MAX_SPEND if needed.` }] };
131
+ }
132
+ if (result.status >= 400) {
133
+ content.push({ type: "text", text: `HTTP ${result.status}: ${result.text.slice(0, 2000)}` });
134
+ if (result.payment) {
135
+ content.push({ type: "text", text: `Note: payment of ${fmtAmount(result.payment.amount, 6)} was already broadcast (tx ${result.payment.hash}) — the operator may refund.` });
136
+ }
137
+ return { isError: true, content };
138
+ }
139
+ content.push({ type: "text", text: result.text.slice(0, 200_000) });
140
+ return { content };
141
+ });
142
+ }
143
+ server.tool("thaifi_spend_cap", "Show the current per-call spend cap (THAIFI_MCP_MAX_SPEND) and whether this machine is paired.", {}, async () => ({
144
+ content: [
145
+ {
146
+ type: "text",
147
+ text: JSON.stringify({
148
+ paired: Boolean(loadStore()?.userAddress),
149
+ wallet: loadStore()?.userAddress ?? null,
150
+ registry: registryUrl(),
151
+ max_spend_per_call: `${fmtAmount(maxSpend, 6)} THCFI`,
152
+ env: "THAIFI_MCP_MAX_SPEND (USD)",
153
+ }, null, 2),
154
+ },
155
+ ],
156
+ }));
157
+ err(`thaifi mcp: ${catalog.length} tools from ${registryUrl()} (max spend ${fmtAmount(maxSpend, 6)}/call, wallet ${store?.userAddress ?? "not paired"})`);
158
+ await server.connect(new StdioServerTransport());
159
+ // Keep the process alive while the transport is open.
160
+ await new Promise(() => { });
161
+ }
@@ -2,23 +2,16 @@
2
2
  import { concatHex, keccak256, slice, stringToBytes } from "viem";
3
3
  import { loadStore } from "../store.js";
4
4
  import { createAgentClient, pickFeeToken } from "../chain.js";
5
- import { buildAgentAuthCredential } from "../agentAuth.js";
5
+ import { b64uEncode, buildAgentAuthCredential, parseAgentChallenge, } from "../agentAuth.js";
6
6
  import { CONFIG, PATHUSD_DECIMALS } from "../config.js";
7
7
  import { PATHUSD, resolveToken } from "../tokens.js";
8
- function b64uDecode(s) {
9
- return Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8");
10
- }
11
- function b64uEncode(s) {
12
- return Buffer.from(s, "utf8")
13
- .toString("base64")
14
- .replace(/\+/g, "-")
15
- .replace(/\//g, "_")
16
- .replace(/=+$/, "");
17
- }
18
8
  /**
19
9
  * Parse an MPP `WWW-Authenticate: Payment id="…", request="<b64u json>"` header.
20
10
  * See https://mpp.dev — Machine Payments Protocol.
21
11
  */
12
+ function b64uDecode(s) {
13
+ return Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8");
14
+ }
22
15
  export function parseMppChallenge(header) {
23
16
  const m = /^Payment\s+(.+)$/i.exec(header.trim());
24
17
  if (!m)
@@ -53,26 +46,6 @@ export function parseMppChallenge(header) {
53
46
  challenge.expires = params.expires;
54
47
  return challenge;
55
48
  }
56
- /**
57
- * Parse a `WWW-Authenticate: ThaiFiAgent realm="…", challenge="<b64u>"`
58
- * header — ThaiFi Agent Auth (identity proof, no payment). The server
59
- * verifies our signature against the AccountKeychain precompile.
60
- */
61
- function parseAgentChallenge(header) {
62
- const m = /^ThaiFiAgent\s+(.+)$/i.exec(header.trim());
63
- if (!m)
64
- return null;
65
- const params = {};
66
- const re = /([a-zA-Z]+)=((?:"((?:[^"\\]|\\.)*)")|[^\s,]+)/g;
67
- let pm;
68
- while ((pm = re.exec(m[1]))) {
69
- const value = pm[3] !== undefined ? pm[3].replace(/\\(.)/g, "$1") : pm[2];
70
- params[pm[1].toLowerCase()] = value;
71
- }
72
- if (!params.realm || !params.challenge)
73
- return null;
74
- return { realm: params.realm, nonce: params.challenge };
75
- }
76
49
  /** MPP tempo charge → on-chain TIP-20 transfer (push mode: we broadcast, then send the hash). */
77
50
  function mppTempoChallenge(challenge) {
78
51
  const req = challenge.request;
package/dist/config.js CHANGED
@@ -3,10 +3,10 @@ export const CONFIG = {
3
3
  rpcUrl: process.env.THAIFI_RPC_URL ?? "https://rpc.thaifi.com",
4
4
  walletUrl: process.env.THAIFI_WALLET_URL ?? "https://wallet.thaifi.com",
5
5
  chainId: Number(process.env.THAIFI_CHAIN_ID ?? 17),
6
- /** pathUSD — ThaiFi fee/stable token (TIP-20, 6 decimals). */
6
+ /** pathTHB — ThaiFi fee/stable token (TIP-20, 6 decimals). */
7
7
  pathUsd: (process.env.THAIFI_PATHUSD ??
8
8
  "0x20c0000000000000000000000000000000000000"),
9
- /** Default spending limit: 100 pathUSD. */
9
+ /** Default spending limit: 100 pathTHB. */
10
10
  defaultLimit: 100n * 10n ** 6n,
11
11
  /** Default limit period: 30 days (seconds). */
12
12
  defaultPeriod: 60 * 60 * 24 * 30,
package/dist/index.js CHANGED
@@ -9,7 +9,9 @@ import { balance } from "./commands/balance.js";
9
9
  import { transfer } from "./commands/transfer.js";
10
10
  import { request } from "./commands/request.js";
11
11
  import { services } from "./commands/services.js";
12
+ import { mcp } from "./commands/mcp.js";
12
13
  import { fund } from "./commands/fund.js";
14
+ import { deposit } from "./commands/deposit.js";
13
15
  import { tokens } from "./commands/tokens.js";
14
16
  // Resolve ../package.json at runtime (works from src/ via tsx and from dist/ when installed) — never hardcode the version here again.
15
17
  const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
@@ -38,21 +40,30 @@ program
38
40
  program
39
41
  .command("balance")
40
42
  .description("Show TIP-20 token balances of the paired account")
41
- .option("--token <symbol|address>", "Token symbol (pathUSD, THCFI, THCOC) or contract address (default: all)")
43
+ .option("--token <symbol|address>", "Token symbol (pathTHB, THCFI, THCOC) or contract address (default: all)")
42
44
  .action(balance);
43
45
  program
44
46
  .command("transfer")
45
47
  .description("Transfer tokens signed by the agent access key (spend limit enforced on-chain)")
46
48
  .argument("<to>", "Recipient address (0x…)")
47
49
  .argument("<amount>", "Amount in human units (e.g. 1.5)")
48
- .option("--token <symbol|address>", "Token symbol or contract address (default: pathUSD)")
49
- .option("--fee-token <symbol|address>", "Gas token (default: auto — pathUSD, else THCFI/THCOC by balance)")
50
+ .option("--token <symbol|address>", "Token symbol or contract address (default: pathTHB)")
51
+ .option("--fee-token <symbol|address>", "Gas token (default: auto — pathTHB, else THCFI/THCOC by balance)")
50
52
  .action(transfer);
51
53
  program
52
54
  .command("fund")
53
55
  .description("Show your deposit address and open the web Deposit page")
54
56
  .option("--no-browser", "Print the address without opening a browser")
55
57
  .action(fund);
58
+ program
59
+ .command("deposit")
60
+ .description("PromptPay QR top-up (1 THB = 1 token): create a QR order, show it for the user to scan, and wait until the tokens are minted")
61
+ .option("--amount <thb>", "Amount in THB (6-10000)")
62
+ .option("--token <symbol>", "Payout token: THCFI (default) or THCOC")
63
+ .option("--ref <referenceNo>", "Watch an existing order instead of creating a new one")
64
+ .option("--no-open", "Do not open the saved QR image in a viewer")
65
+ .option("--json", "Machine-readable output (JSON)")
66
+ .action(deposit);
56
67
  program
57
68
  .command("request")
58
69
  .description("HTTP request with automatic x402/MPP payments (curl-like)")
@@ -63,7 +74,7 @@ program
63
74
  .option("--json <json>", "JSON body shorthand (sets method POST + content-type)")
64
75
  .option("--dry-run", "Print the request without sending")
65
76
  .option("--max-spend <usd>", "Maximum automatic payment (USD)", Number)
66
- .option("--fee-token <symbol|address>", "Gas token for the payment tx (default: auto — pathUSD, else THCFI/THCOC by balance)")
77
+ .option("--fee-token <symbol|address>", "Gas token for the payment tx (default: auto — pathTHB, else THCFI/THCOC by balance)")
67
78
  .action(async (url, opts) => {
68
79
  const headers = {};
69
80
  for (const h of opts.header ?? []) {
@@ -91,4 +102,8 @@ program
91
102
  .description("Discover paid services from the MPP/x402 registry")
92
103
  .option("--search <query>", "Search services")
93
104
  .action(services);
105
+ program
106
+ .command("mcp")
107
+ .description("Run a stdio MCP server exposing all MPP catalog services as tools (pays with the paired access key)")
108
+ .action(mcp);
94
109
  program.parseAsync();
@@ -0,0 +1,256 @@
1
+ /**
2
+ * HTTP fetch that pays MPP challenges with the paired agent access key.
3
+ *
4
+ * Same wire logic as `thaifi request` (src/commands/request.ts is the
5
+ * reference implementation, kept untouched) but as a quiet, programmable
6
+ * function used by `thaifi mcp` — no stdout logging, returns a result object.
7
+ *
8
+ * Flow (MPP tempo charge, push mode):
9
+ * 1. normal fetch; if not 402 → return as-is
10
+ * 2. parse the `WWW-Authenticate: Payment …` challenge (x402 legacy fallback)
11
+ * 3. broadcast the TIP-20 transfer ourselves (memo-bound to the challenge)
12
+ * 4. retry with `Authorization: Payment <base64url credential>`
13
+ */
14
+ import { keccak256, slice, stringToBytes, concatHex } from "viem";
15
+ import { loadStore } from "./store.js";
16
+ import { CONFIG, PATHUSD_DECIMALS } from "./config.js";
17
+ import { createAgentClient, pickFeeToken } from "./chain.js";
18
+ import { b64uEncode } from "./agentAuth.js";
19
+ /** b64url → bytes (mirrors agentAuth's internal helper, which is not exported). */
20
+ function b64uDecodeBytes(value) {
21
+ return new Uint8Array(Buffer.from(value.replace(/-/g, "+").replace(/_/g, "/"), "base64"));
22
+ }
23
+ // ── MPP attribution memo (32 bytes) per mppx Attribution encoding ─────────
24
+ // tag = keccak256("mpp") [0..4 bytes], MPP protocol version 0x10,
25
+ // serverId = realm fingerprint, clientId = "thaifi-wallet-cli" fingerprint,
26
+ // nonce = keccak256(challengeId) [0..7 bytes].
27
+ // Required by servers that don't pre-assign a memo to the challenge.
28
+ const MPP_TAG = slice(keccak256(stringToBytes("mpp")), 0, 4);
29
+ const fingerprint = (value) => slice(keccak256(stringToBytes(value)), 0, 10);
30
+ function attributionMemo(realm, challengeId) {
31
+ return concatHex([
32
+ MPP_TAG,
33
+ "0x10",
34
+ fingerprint(realm),
35
+ fingerprint("thaifi-wallet-cli"),
36
+ slice(keccak256(stringToBytes(challengeId)), 0, 7),
37
+ ]);
38
+ }
39
+ function storeAddress() {
40
+ const store = loadStore();
41
+ return store?.userAddress ?? "0x0";
42
+ }
43
+ function parseMppChallenge(header) {
44
+ // mppx emits scheme "Payment" (some servers use "MPP") — both are the same param set.
45
+ const m = /^(?:Payment|MPP)\s+(.+)$/i.exec(header.trim());
46
+ if (!m)
47
+ return null;
48
+ const params = {};
49
+ const re = /([a-zA-Z]+)=(?:"((?:[^"\\]|\\.)*)"|([^\s,]+))/g;
50
+ let pm;
51
+ while ((pm = re.exec(m[1]))) {
52
+ const value = pm[3] !== undefined ? pm[3] : pm[2] !== undefined ? pm[2].replace(/\\(.)/g, "$1") : "";
53
+ params[pm[1].toLowerCase()] = value;
54
+ }
55
+ if (!params.request || !params.realm)
56
+ return null;
57
+ let request;
58
+ try {
59
+ request = JSON.parse(Buffer.from(b64uDecodeBytes(params.request)).toString("utf8"));
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ const challenge = {
65
+ id: params.id,
66
+ realm: params.realm ?? "",
67
+ method: params.method ?? "tempo",
68
+ intent: params.intent ?? "charge",
69
+ request,
70
+ requestRaw: params.request,
71
+ };
72
+ if (params.expires !== undefined)
73
+ challenge.expires = params.expires;
74
+ if (params.opaque !== undefined)
75
+ challenge.opaque = params.opaque;
76
+ return challenge;
77
+ }
78
+ /** MPP tempo charge → on-chain TIP-20 transfer (push mode: we broadcast, then send the hash). */
79
+ function mppTempoChallenge(challenge) {
80
+ const req = challenge.request;
81
+ if (!req?.amount || !req?.recipient)
82
+ return null;
83
+ const amount = BigInt(req.amount);
84
+ // mppx omits `decimals` for pathTHB-denominated challenges.
85
+ const decimals = req.decimals ?? 6;
86
+ return {
87
+ payTo: req.recipient,
88
+ amount,
89
+ token: req.currency,
90
+ memo: req.memo ?? attributionMemo(challenge.realm, challenge.id),
91
+ decimals,
92
+ proofHeader: (txHash) => {
93
+ // Write format: challenge.request is the base64url string, not a re-sent object.
94
+ const credential = {
95
+ challenge: {
96
+ id: challenge.id,
97
+ realm: challenge.realm,
98
+ method: challenge.method,
99
+ intent: challenge.intent,
100
+ request: challenge.requestRaw,
101
+ ...(challenge.opaque !== undefined && { opaque: challenge.opaque }),
102
+ ...(challenge.expires !== undefined && { expires: challenge.expires }),
103
+ },
104
+ payload: { type: "hash", hash: txHash },
105
+ source: `did:pkh:eip155:${CONFIG.chainId}:${storeAddress()}`,
106
+ };
107
+ return "Payment " + b64uEncode(JSON.stringify(credential));
108
+ },
109
+ };
110
+ }
111
+ /**
112
+ * Extract a payment challenge from a 402 response.
113
+ * MPP first (`WWW-Authenticate: Payment …`), legacy x402 JSON body as fallback.
114
+ */
115
+ export async function parseChallenge(res, defaultToken) {
116
+ const wwwAuth = res.headers.get("www-authenticate");
117
+ if (wwwAuth) {
118
+ const mpp = parseMppChallenge(wwwAuth);
119
+ if (mpp && mpp.method === "tempo" && mpp.intent === "charge") {
120
+ const parsed = mppTempoChallenge(mpp);
121
+ if (parsed)
122
+ return parsed;
123
+ }
124
+ let body = null;
125
+ try {
126
+ body = await res.json();
127
+ }
128
+ catch {
129
+ body = null;
130
+ }
131
+ const accept = body?.payments?.[0] ?? body?.accepts?.[0] ?? body;
132
+ const payTo = accept?.payTo ?? accept?.payToAddress ?? accept?.recipient;
133
+ const maxAmount = accept?.maxAmountRequired ?? accept?.amount ?? accept?.price;
134
+ if (!payTo || maxAmount === undefined)
135
+ return null;
136
+ const amount = typeof maxAmount === "string" && /^\d+$/.test(maxAmount)
137
+ ? BigInt(maxAmount)
138
+ : BigInt(Math.round(Number(maxAmount) * 10 ** PATHUSD_DECIMALS));
139
+ return {
140
+ payTo,
141
+ amount,
142
+ token: accept?.asset ?? accept?.token ?? defaultToken,
143
+ decimals: PATHUSD_DECIMALS,
144
+ proofHeader: (txHash) => Buffer.from(JSON.stringify({
145
+ scheme: "exact",
146
+ txHash,
147
+ from: storeAddress(),
148
+ to: payTo,
149
+ amount: "0x" + amount.toString(16),
150
+ token: accept?.asset ?? accept?.token ?? defaultToken,
151
+ })).toString("base64"),
152
+ };
153
+ }
154
+ return null;
155
+ }
156
+ /** Format the challenge amount for humans (uses the challenge's own decimals). */
157
+ export function fmtAmount(amount, decimals) {
158
+ const base = 10n ** BigInt(decimals);
159
+ const whole = amount / base;
160
+ const frac = amount % base;
161
+ return frac === 0n ? whole.toString() : `${whole}.${frac.toString().padStart(decimals, "0")}`.replace(/0+$/, "").replace(/\.$/, "");
162
+ }
163
+ /**
164
+ * Fetch with automatic MPP payment. Payment is signed locally by the agent
165
+ * access key and enforced on-chain by the AccountKeychain precompile.
166
+ */
167
+ export async function paidFetch(url, opts = {}) {
168
+ const log = opts.log ?? (() => { });
169
+ const store = loadStore();
170
+ if (!store?.userAddress || !store?.privateKey) {
171
+ return unpaid402(url, "Not paired — run `thaifi login` first.", opts.log);
172
+ }
173
+ const method = opts.method ?? "GET";
174
+ const res = await fetch(url, {
175
+ method,
176
+ headers: {
177
+ "user-agent": "thaifi-wallet-cli",
178
+ ...(opts.body !== undefined ? { "content-type": "application/json" } : {}),
179
+ ...opts.headers,
180
+ },
181
+ body: opts.body,
182
+ });
183
+ const contentType = res.headers.get("content-type");
184
+ if (res.status !== 402) {
185
+ return { status: res.status, contentType, text: await res.text() };
186
+ }
187
+ // 402 Payment Required — parse the challenge and pay it.
188
+ const challenge = await parseChallenge(res.clone(), CONFIG.pathUsd);
189
+ if (!challenge) {
190
+ const snippet = (await res.text()).slice(0, 400);
191
+ return unpaid402(url, `402 Payment Required — could not parse the challenge. ${snippet}`, opts.log);
192
+ }
193
+ if (opts.maxSpend !== undefined && challenge.amount > opts.maxSpend) {
194
+ return unpaid402(url, `Quote ${fmtAmount(challenge.amount, challenge.decimals ?? 6)} exceeds this session's max spend (${fmtAmount(opts.maxSpend, 6)}) — refusing to pay.`, opts.log);
195
+ }
196
+ const token = (challenge.token ?? CONFIG.pathUsd);
197
+ log(`Paying ${fmtAmount(challenge.amount, challenge.decimals ?? 6)} (base ${challenge.amount}) to ${challenge.payTo}…`);
198
+ // pickFeeToken needs a client; probe with the pathTHB default, then re-create
199
+ // the agent client with the chosen fee token (chain FeeAMM swaps it to the
200
+ // validator's pathTHB).
201
+ let feeToken;
202
+ try {
203
+ if (opts.feeToken) {
204
+ feeToken = opts.feeToken;
205
+ }
206
+ else {
207
+ const probe = createAgentClient(store, CONFIG.pathUsd);
208
+ const info = await pickFeeToken(probe, store.userAddress);
209
+ feeToken = info.address;
210
+ }
211
+ }
212
+ catch (err) {
213
+ return unpaid402(url, `Could not pick a gas token: ${err instanceof Error ? err.message : String(err)}`, opts.log);
214
+ }
215
+ const client = createAgentClient(store, feeToken);
216
+ let hash;
217
+ try {
218
+ hash = await client.token.transfer({
219
+ to: challenge.payTo,
220
+ amount: challenge.amount,
221
+ token,
222
+ memo: challenge.memo,
223
+ });
224
+ }
225
+ catch (err) {
226
+ return unpaid402(url, `Payment transaction failed: ${err instanceof Error ? err.message : String(err)}`, opts.log);
227
+ }
228
+ log(`Payment tx ${hash} — waiting for receipt…`);
229
+ const receipt = await client.waitForTransactionReceipt({ hash });
230
+ if (receipt.status !== "success") {
231
+ log(`⚠️ Payment transaction reverted: ${hash}`);
232
+ return { status: res.status, contentType, text: "Payment transaction reverted — no charge applied.", payment: undefined, unpaid402: `reverted: ${hash}` };
233
+ }
234
+ const retry = await fetch(url, {
235
+ method,
236
+ headers: {
237
+ "user-agent": "thaifi-wallet-cli",
238
+ ...(opts.body !== undefined ? { "content-type": "application/json" } : {}),
239
+ ...opts.headers,
240
+ authorization: challenge.proofHeader(hash),
241
+ },
242
+ body: opts.body,
243
+ });
244
+ const retryText = await retry.text();
245
+ log(`Paid. Retry → HTTP ${retry.status}`);
246
+ return {
247
+ status: retry.status,
248
+ contentType: retry.headers.get("content-type"),
249
+ text: retryText,
250
+ payment: { token, amount: challenge.amount, hash },
251
+ };
252
+ }
253
+ function unpaid402(url, message, log) {
254
+ log?.(message);
255
+ return { status: 402, contentType: null, text: JSON.stringify({ error: "unpaid", detail: message }), unpaid402: message };
256
+ }
package/dist/tokens.js CHANGED
@@ -1,9 +1,9 @@
1
1
  /** TIP-20 tokens surfaced by the CLI — all ThaiFi TIP-20s use 6 decimals. */
2
2
  import { CONFIG } from "./config.js";
3
- export const PATHUSD = {
3
+ export const PATHTHB = {
4
4
  address: CONFIG.pathUsd,
5
- symbol: "pathUSD",
6
- name: "pathUSD",
5
+ symbol: "pathTHB",
6
+ name: "pathTHB",
7
7
  decimals: 6,
8
8
  };
9
9
  // System TIP-20 tokens (native factory, 2026-09-24) — can pay gas fees via
@@ -21,13 +21,17 @@ export const THCOC = {
21
21
  decimals: 6,
22
22
  };
23
23
  /** Tokens listed by `thaifi tokens` / shown by default in `thaifi balance`. */
24
- export const TOKENS = [PATHUSD, THCFI, THCOC];
24
+ export const TOKENS = [PATHTHB, THCFI, THCOC];
25
+ /** Pre-rename alias — old scripts using `pathUSD` still resolve. */
26
+ export const PATHUSD = PATHTHB;
25
27
  /**
26
28
  * Resolve a token by symbol (case-insensitive) or 0x address. Addresses
27
29
  * outside the registry are treated as custom TIP-20s (6 decimals); the
28
30
  * symbol is read from the chain by the caller.
29
31
  */
30
32
  export function resolveToken(arg) {
33
+ if (arg.toLowerCase() === "pathusd")
34
+ return PATHTHB; // pre-rename alias
31
35
  const bySymbol = TOKENS.find((t) => t.symbol.toLowerCase() === arg.toLowerCase());
32
36
  if (bySymbol)
33
37
  return bySymbol;
@@ -37,5 +41,5 @@ export function resolveToken(arg) {
37
41
  return known;
38
42
  return { address: arg, symbol: "TOKEN", name: "Custom TIP-20", decimals: 6 };
39
43
  }
40
- throw new Error(`Unknown token "${arg}" — use a symbol (pathUSD, THCFI, THCOC) or a 0x contract address.`);
44
+ throw new Error(`Unknown token "${arg}" — use a symbol (pathTHB, THCFI, THCOC) or a 0x contract address.`);
41
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thaifi-wallet-cli",
3
- "version": "0.3.6",
3
+ "version": "0.5.0",
4
4
  "description": "ThaiFi Wallet CLI for AI agents — pair with wallet.thaifi.com, sign with on-chain access keys (spending limits enforced on-chain), x402/MPP payments",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,6 +24,7 @@
24
24
  "ai",
25
25
  "x402",
26
26
  "mpp",
27
+ "mcp",
27
28
  "tempo",
28
29
  "cli",
29
30
  "payments"
@@ -33,11 +34,17 @@
33
34
  "node": ">=20"
34
35
  },
35
36
  "dependencies": {
37
+ "@modelcontextprotocol/sdk": "^1.24.0",
36
38
  "commander": "^14.0.0",
37
- "viem": "^2.56.0"
39
+ "jsqr": "^1.4.0",
40
+ "pngjs": "^7.0.0",
41
+ "qrcode-terminal": "^0.12.0",
42
+ "viem": "^2.56.0",
43
+ "zod": "^3.25.0"
38
44
  },
39
45
  "devDependencies": {
40
46
  "@types/node": "^24.13.2",
47
+ "@types/pngjs": "^6.0.5",
41
48
  "tsx": "^4.19.0",
42
49
  "typescript": "^5.9.0"
43
50
  }