@quackai/q402-mcp 0.8.31 → 0.8.33

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 +5 -2
  2. package/dist/index.js +320 -3
  3. package/package.json +4 -2
package/README.md CHANGED
@@ -156,7 +156,7 @@ Then export the values in `~/.zshrc` / `~/.bashrc`. See the [Codex config refere
156
156
 
157
157
  ## Tools exposed
158
158
 
159
- **24 tools** — read-only by default; live mode needs an API key + signing path + `Q402_ENABLE_REAL_PAYMENTS=1`.
159
+ **27 tools** — read-only by default; live mode needs an API key + signing path + `Q402_ENABLE_REAL_PAYMENTS=1`.
160
160
 
161
161
  | Tool | Auth | Purpose |
162
162
  |---|---|---|
@@ -184,8 +184,11 @@ Then export the values in `~/.zshrc` / `~/.bashrc`. See the [Codex config refere
184
184
  | `q402_yield_positions` | api key | Show the Agent Wallet's open Q402 Yield positions (balance, principal, accrued interest, APY) + total supplied in USD. Mode C. |
185
185
  | `q402_yield_deposit` | live mode | Supply the Agent Wallet's USDC/USDT into Aave V3 (Q402 Yield) to earn supply APY. Mode C, BNB-only. Requires `confirm: true`; sandbox-by-default. |
186
186
  | `q402_yield_withdraw` | live mode | Withdraw supplied USDC/USDT out of Aave V3 back to the Agent Wallet (`amount: "max"` = full position). Mode C, BNB-only. Requires `confirm: true`; sandbox-by-default. |
187
+ | `q402_request_create` | api key | Publish a payment request (invoice). No funds move; returns a shareable `/pay` link + `req_…` id. Recipient defaults to the Agent Wallet. |
188
+ | `q402_request_status` | none | Look up a payment request by `req_…` id (amount, token, chain, recipient, status). Read-only; `notFound` instead of throwing. |
189
+ | `q402_request_pay` | live mode | Pay a request gaslessly from the payer's own Agent Wallet (Mode C). Terms come from the stored request, so they can't be redirected. Two-phase consent (same as `q402_pay`). |
187
190
 
188
- `q402_pay` + `q402_batch_pay` + `q402_bridge_send` + `q402_yield_deposit` + `q402_yield_withdraw` require explicit in-chat confirmation. Batch confirmation = full batch, not per-row.
191
+ `q402_pay` + `q402_batch_pay` + `q402_bridge_send` + `q402_yield_deposit` + `q402_yield_withdraw` + `q402_request_pay` require explicit in-chat confirmation. Batch confirmation = full batch, not per-row.
189
192
 
190
193
  > ℹ️ `q402_pay` expects a 0x address — ENS isn't resolved server-side. Resolve client-side first.
191
194
  > Per-chain Gas Tank balances + full TX history live in the [dashboard](https://q402.quackai.ai/dashboard) (wallet-signature only).
package/dist/index.js CHANGED
@@ -211,7 +211,7 @@ var isValidPrivateKey = (s) => typeof s === "string" && PRIVATE_KEY_RE.test(s);
211
211
  // package.json
212
212
  var package_default = {
213
213
  name: "@quackai/q402-mcp",
214
- version: "0.8.31",
214
+ version: "0.8.33",
215
215
  description: "MCP server for Q402 \u2014 gasless USDC/USDT/RLUSD payments on 10 EVM chains + Chainlink CCIP USDC bridge on the eth/avax/arbitrum triangle, callable from Claude (Desktop / Code), OpenAI Codex CLI, and any other Model Context Protocol client.",
216
216
  mcpName: "io.github.bitgett/q402-mcp",
217
217
  keywords: [
@@ -279,7 +279,9 @@ var package_default = {
279
279
  access: "public"
280
280
  },
281
281
  overrides: {
282
- esbuild: "^0.28.1"
282
+ esbuild: "^0.28.1",
283
+ ws: "^8.21.0",
284
+ hono: "^4.12.25"
283
285
  }
284
286
  };
285
287
 
@@ -4778,6 +4780,302 @@ async function runRecurringSkipNext(input) {
4778
4780
  }
4779
4781
  }
4780
4782
 
4783
+ // src/tools/request-create.ts
4784
+ import { z as z25 } from "zod";
4785
+ var ADDRESS_RE2 = /^0x[0-9a-fA-F]{40}$/;
4786
+ var AMOUNT_RE2 = /^\d+(\.\d{1,18})?$/;
4787
+ var RequestCreateInputSchema = z25.object({
4788
+ amount: z25.string().regex(AMOUNT_RE2).describe('Amount to request, as a decimal string (e.g. "5", "1.50"). Counted in `token` (USDC or USDT, both USD-1).'),
4789
+ token: z25.enum(["USDC", "USDT"]).default("USDT").describe("Stablecoin to be paid. USDC or USDT. Both peg USD-1."),
4790
+ chain: z25.enum(["bnb", "eth", "avax", "xlayer", "stable", "mantle", "injective", "monad", "scroll", "arbitrum"]).default("bnb").describe("Chain the request settles on. Defaults to bnb."),
4791
+ recipient: z25.string().regex(ADDRESS_RE2).optional().describe("0x address that receives the funds. Defaults to Q402_AGENT_WALLET_ADDRESS (the agent bills itself) when omitted."),
4792
+ memo: z25.string().max(200).optional().describe("Optional note shown to the payer (\u2264200 chars), e.g. an invoice number or service description."),
4793
+ ttlDays: z25.number().int().min(1).max(90).optional().describe("Days until the request expires. Defaults to 7 (max 90).")
4794
+ });
4795
+ async function runRequestCreate(input) {
4796
+ const base = CONFIG.relayBaseUrl;
4797
+ if (!CONFIG.apiKey) {
4798
+ return {
4799
+ ok: false,
4800
+ requestId: null,
4801
+ payUrl: null,
4802
+ request: null,
4803
+ error: "API_KEY_REQUIRED",
4804
+ message: "No Q402 API key configured. Run q402_doctor to set one up."
4805
+ };
4806
+ }
4807
+ const recipient = input.recipient?.toLowerCase() ?? CONFIG.walletId;
4808
+ if (!recipient) {
4809
+ return {
4810
+ ok: false,
4811
+ requestId: null,
4812
+ payUrl: null,
4813
+ request: null,
4814
+ error: "RECIPIENT_REQUIRED",
4815
+ message: "No recipient given and no Q402_AGENT_WALLET_ADDRESS configured. Pass `recipient` (the 0x address that should receive the funds)."
4816
+ };
4817
+ }
4818
+ try {
4819
+ const res = await fetch(`${base}/request`, {
4820
+ method: "POST",
4821
+ headers: { "Content-Type": "application/json" },
4822
+ body: JSON.stringify({
4823
+ apiKey: CONFIG.apiKey,
4824
+ chain: input.chain ?? "bnb",
4825
+ token: input.token ?? "USDT",
4826
+ amount: input.amount,
4827
+ recipient,
4828
+ ...input.memo !== void 0 ? { memo: input.memo } : {},
4829
+ ...input.ttlDays !== void 0 ? { ttlDays: input.ttlDays } : {}
4830
+ }),
4831
+ signal: AbortSignal.timeout(15e3)
4832
+ });
4833
+ const data = await res.json().catch(() => ({}));
4834
+ if (!res.ok || !data.requestId) {
4835
+ return {
4836
+ ok: false,
4837
+ requestId: null,
4838
+ payUrl: null,
4839
+ request: null,
4840
+ error: data.error ?? `HTTP_${res.status}`,
4841
+ message: data.error ?? `Create failed with HTTP ${res.status}.`
4842
+ };
4843
+ }
4844
+ return {
4845
+ ok: true,
4846
+ requestId: data.requestId,
4847
+ payUrl: data.payUrl ?? null,
4848
+ request: data.request ?? null
4849
+ };
4850
+ } catch (e) {
4851
+ return {
4852
+ ok: false,
4853
+ requestId: null,
4854
+ payUrl: null,
4855
+ request: null,
4856
+ error: "NETWORK_ERROR",
4857
+ message: e instanceof Error ? e.message : String(e)
4858
+ };
4859
+ }
4860
+ }
4861
+ var REQUEST_CREATE_TOOL = {
4862
+ name: "q402_request_create",
4863
+ description: "Publish a Q402 payment request (an invoice / bill). Moves no funds \u2014 it creates a shareable request to RECEIVE money \u2014 so no confirmation is needed and a Trial key works. Returns a req_ id + a /pay link you can share with a human, or hand the requestId to another agent that pays it gaslessly via q402_request_pay. The recipient defaults to your configured Agent Wallet, so you can bill yourself with just an amount. Pair with q402_request_status to poll for payment.",
4864
+ inputSchema: {
4865
+ type: "object",
4866
+ properties: {
4867
+ amount: {
4868
+ type: "string",
4869
+ pattern: "^\\d+(\\.\\d{1,18})?$",
4870
+ description: 'Required. Amount to request as a decimal string (e.g. "5", "1.50").'
4871
+ },
4872
+ token: {
4873
+ type: "string",
4874
+ enum: ["USDC", "USDT"],
4875
+ description: "Default 'USDT'. Both peg USD-1."
4876
+ },
4877
+ chain: {
4878
+ type: "string",
4879
+ enum: ["bnb", "eth", "avax", "xlayer", "stable", "mantle", "injective", "monad", "scroll", "arbitrum"],
4880
+ description: "Default 'bnb'. Chain the request settles on."
4881
+ },
4882
+ recipient: {
4883
+ type: "string",
4884
+ pattern: "^0x[0-9a-fA-F]{40}$",
4885
+ description: "Optional 0x address to receive funds. Defaults to Q402_AGENT_WALLET_ADDRESS (bill yourself)."
4886
+ },
4887
+ memo: {
4888
+ type: "string",
4889
+ maxLength: 200,
4890
+ description: "Optional note shown to the payer (\u2264200 chars)."
4891
+ },
4892
+ ttlDays: {
4893
+ type: "number",
4894
+ minimum: 1,
4895
+ maximum: 90,
4896
+ description: "Days until expiry. Default 7."
4897
+ }
4898
+ },
4899
+ required: ["amount"],
4900
+ additionalProperties: false
4901
+ }
4902
+ };
4903
+
4904
+ // src/tools/request-status.ts
4905
+ import { z as z26 } from "zod";
4906
+ var RequestStatusInputSchema = z26.object({
4907
+ requestId: z26.string().regex(/^req_[0-9a-f]{24}$/, "requestId must match req_<24-hex>").describe("Payment request id (req_ + 24 hex chars). Returned by q402_request_create; also the tail of any /pay/ URL.")
4908
+ });
4909
+ async function runRequestStatus(input) {
4910
+ const base = CONFIG.relayBaseUrl;
4911
+ const pageBase2 = base.replace(/\/api\/?$/, "");
4912
+ const resp = await fetch(`${base}/request/${input.requestId}`, {
4913
+ signal: AbortSignal.timeout(1e4)
4914
+ });
4915
+ if (resp.status === 404) {
4916
+ return { found: false, request: null, payUrl: null, notFound: true };
4917
+ }
4918
+ if (!resp.ok) {
4919
+ throw new Error(`request status fetch failed: HTTP ${resp.status}`);
4920
+ }
4921
+ const data = await resp.json();
4922
+ return {
4923
+ found: true,
4924
+ request: data.request,
4925
+ payUrl: `${pageBase2}/pay/${data.request.id}`
4926
+ };
4927
+ }
4928
+ var REQUEST_STATUS_TOOL = {
4929
+ name: "q402_request_status",
4930
+ description: "Look up a Q402 payment request by its req_ id. Read-only, no API key. Returns the amount, token, chain, recipient, status (open | paid | expired | cancelled) and a shareable pay URL. Use it to poll a request you created, or to inspect a requestId before paying it with q402_request_pay. An unknown or expired id returns notFound:true (no throw).",
4931
+ inputSchema: {
4932
+ type: "object",
4933
+ properties: {
4934
+ requestId: {
4935
+ type: "string",
4936
+ pattern: "^req_[0-9a-f]{24}$",
4937
+ description: "Payment request id (req_ + 24 hex). Returned by q402_request_create; also the tail of a /pay/ URL."
4938
+ }
4939
+ },
4940
+ required: ["requestId"],
4941
+ additionalProperties: false
4942
+ }
4943
+ };
4944
+
4945
+ // src/tools/request-pay.ts
4946
+ import { z as z27 } from "zod";
4947
+ var RequestPayInputSchema = z27.object({
4948
+ requestId: z27.string().regex(/^req_[0-9a-f]{24}$/, "requestId must match req_<24-hex>").describe("The payment request to pay (req_ + 24 hex). Get it from a /pay link, a 402 response, or whoever billed you."),
4949
+ confirm: z27.literal(true).describe(
4950
+ "REQUIRED. Must be literally `true`. Acknowledges this moves real funds from your Agent Wallet."
4951
+ ),
4952
+ consentToken: z27.string().optional().describe(
4953
+ 'Two-phase consent, identical to q402_pay. Call FIRST WITHOUT it: the tool moves no money and returns status="needs_consent" with a `preview` of the exact payment plus a `consentToken`. Relay the preview to the user, get an explicit yes, then re-call with the SAME requestId PLUS this consentToken. The token is re-derived from the request\'s terms, so a previewed payment cannot be swapped for a different one. confirm:true alone does NOT fire a payment.'
4954
+ ),
4955
+ walletId: z27.string().optional().describe("Optional lowercased Agent Wallet address to pay from when you hold multiple. Defaults to Q402_AGENT_WALLET_ADDRESS, then the server default.")
4956
+ });
4957
+ async function runRequestPay(input) {
4958
+ const base = CONFIG.relayBaseUrl;
4959
+ let req;
4960
+ try {
4961
+ const r = await fetch(`${base}/request/${input.requestId}`, { signal: AbortSignal.timeout(1e4) });
4962
+ if (r.status === 404) {
4963
+ return { ok: false, status: "not_payable", requestId: input.requestId, txHash: null, receiptId: null, error: "NOT_FOUND", message: "No request with that id." };
4964
+ }
4965
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
4966
+ req = (await r.json()).request;
4967
+ } catch (e) {
4968
+ return { ok: false, status: "failed", requestId: input.requestId, txHash: null, receiptId: null, error: "LOOKUP_FAILED", message: e instanceof Error ? e.message : String(e) };
4969
+ }
4970
+ const terms = { amount: req.amount, token: req.token, chain: req.chain, recipient: req.recipient };
4971
+ if (req.status !== "open") {
4972
+ return { ok: false, status: "not_payable", requestId: req.id, txHash: null, receiptId: null, ...terms, error: req.status.toUpperCase(), message: `Request is ${req.status} \u2014 nothing to pay.` };
4973
+ }
4974
+ const amountNum = Number(req.amount);
4975
+ if (Number.isFinite(amountNum) && amountNum > CONFIG.maxAmountPerCallUsd) {
4976
+ return { ok: false, status: "not_payable", requestId: req.id, txHash: null, receiptId: null, ...terms, error: "AMOUNT_EXCEEDS_CAP", message: `Request amount $${req.amount} exceeds your Q402_MAX_AMOUNT_PER_CALL cap of $${CONFIG.maxAmountPerCallUsd}.` };
4977
+ }
4978
+ if (CONFIG.allowedRecipients.length > 0 && !CONFIG.allowedRecipients.includes(req.recipient.toLowerCase())) {
4979
+ return { ok: false, status: "not_payable", requestId: req.id, txHash: null, receiptId: null, ...terms, error: "RECIPIENT_NOT_ALLOWED", message: `Recipient ${req.recipient} is not in Q402_ALLOWED_RECIPIENTS.` };
4980
+ }
4981
+ const consentIntent = {
4982
+ t: "request_pay",
4983
+ requestId: req.id,
4984
+ to: req.recipient.toLowerCase(),
4985
+ amount: req.amount,
4986
+ token: req.token,
4987
+ chain: req.chain
4988
+ };
4989
+ const consent = checkConsent(consentIntent, input.consentToken);
4990
+ if (!consent.ok) {
4991
+ return {
4992
+ ok: false,
4993
+ status: "needs_consent",
4994
+ requestId: req.id,
4995
+ txHash: null,
4996
+ receiptId: null,
4997
+ ...terms,
4998
+ message: "Relay this preview to the user and get an explicit yes, then re-call with the same requestId plus consentToken. No funds moved.",
4999
+ needsConsent: {
5000
+ status: "needs_confirmation",
5001
+ preview: `Pay ${req.amount} ${req.token} to ${req.recipient} on ${req.chain} (request ${req.id}).`,
5002
+ consentToken: consent.expected
5003
+ }
5004
+ };
5005
+ }
5006
+ const resolved = resolveApiKey(req.chain, "auto");
5007
+ if (!resolved.apiKey || !resolved.apiKey.startsWith("q402_live_")) {
5008
+ return {
5009
+ ok: false,
5010
+ status: "sandbox",
5011
+ requestId: req.id,
5012
+ txHash: null,
5013
+ receiptId: null,
5014
+ ...terms,
5015
+ error: "LIVE_KEY_REQUIRED",
5016
+ message: "Paying a request settles real funds and needs a live Q402 API key.",
5017
+ setupHint: resolved.sandboxReason ?? "Configure a live Q402_MULTICHAIN_API_KEY (or Q402_TRIAL_API_KEY for BNB) to pay requests."
5018
+ };
5019
+ }
5020
+ if (!CONFIG.realPaymentsRequested) {
5021
+ return {
5022
+ ok: false,
5023
+ status: "sandbox",
5024
+ requestId: req.id,
5025
+ txHash: null,
5026
+ receiptId: null,
5027
+ ...terms,
5028
+ error: "REAL_PAYMENTS_DISABLED",
5029
+ message: "Real payments are off, so this request was not paid.",
5030
+ setupHint: "Set Q402_ENABLE_REAL_PAYMENTS=1 to let q402_request_pay settle real funds."
5031
+ };
5032
+ }
5033
+ const walletId = typeof input.walletId === "string" && input.walletId.length > 0 ? input.walletId.toLowerCase() : CONFIG.walletId;
5034
+ try {
5035
+ const res = await fetch(`${base}/request/${req.id}/pay`, {
5036
+ method: "POST",
5037
+ headers: { "Content-Type": "application/json" },
5038
+ body: JSON.stringify({
5039
+ payerApiKey: resolved.apiKey,
5040
+ ...walletId ? { walletId } : {}
5041
+ }),
5042
+ signal: AbortSignal.timeout(6e4)
5043
+ });
5044
+ const data = await res.json().catch(() => ({}));
5045
+ if (!res.ok || !data.txHash) {
5046
+ return { ok: false, status: "failed", requestId: req.id, txHash: null, receiptId: null, ...terms, error: data.error ?? `HTTP_${res.status}`, message: data.message ?? data.error ?? `Settlement failed (HTTP ${res.status}).` };
5047
+ }
5048
+ return { ok: true, status: "paid", requestId: req.id, txHash: data.txHash, receiptId: data.receiptId ?? null, ...terms };
5049
+ } catch (e) {
5050
+ return { ok: false, status: "failed", requestId: req.id, txHash: null, receiptId: null, ...terms, error: "NETWORK_ERROR", message: e instanceof Error ? e.message : String(e) };
5051
+ }
5052
+ }
5053
+ var REQUEST_PAY_TOOL = {
5054
+ name: "q402_request_pay",
5055
+ description: "Pay a Q402 payment request from your own Agent Wallet, gaslessly. Give it a req_ id (from a /pay link, a 402 Payment Required response, or whoever billed you) and it settles the exact amount + token + recipient the request specifies \u2014 you cannot redirect or change them. MOVES FUNDS: requires confirm:true, a live API key, and Q402_ENABLE_REAL_PAYMENTS=1, same as q402_pay. Call q402_request_status first to show the user what they're paying. This is the agent-to-agent billing path: agent A bills with q402_request_create, agent B settles here.",
5056
+ inputSchema: {
5057
+ type: "object",
5058
+ properties: {
5059
+ requestId: {
5060
+ type: "string",
5061
+ pattern: "^req_[0-9a-f]{24}$",
5062
+ description: "Required. The req_ id to pay."
5063
+ },
5064
+ confirm: {
5065
+ type: "boolean",
5066
+ const: true,
5067
+ description: "REQUIRED. Must be literally true. Paying moves real funds \u2014 get an explicit user yes first."
5068
+ },
5069
+ walletId: {
5070
+ type: "string",
5071
+ description: "Optional. Agent Wallet address to pay from. Defaults to the configured / server-default wallet."
5072
+ }
5073
+ },
5074
+ required: ["requestId", "confirm"],
5075
+ additionalProperties: false
5076
+ }
5077
+ };
5078
+
4781
5079
  // src/index.ts
4782
5080
  function jsonText(value) {
4783
5081
  return { type: "text", text: JSON.stringify(value, null, 2) };
@@ -4827,7 +5125,14 @@ async function main() {
4827
5125
  // (like q402_pay). Mode C: apiKey in the body, server signs the supply
4828
5126
  // / withdraw with the encrypted key.
4829
5127
  YIELD_DEPOSIT_TOOL,
4830
- YIELD_WITHDRAW_TOOL
5128
+ YIELD_WITHDRAW_TOOL,
5129
+ // Payment Requests — the receive side. create publishes an invoice
5130
+ // (no funds move, apiKey only), status is a public id lookup, pay
5131
+ // settles a request gaslessly from the payer's own Agent Wallet
5132
+ // (Mode C, confirm-gated like q402_pay). Enables agent-to-agent billing.
5133
+ REQUEST_CREATE_TOOL,
5134
+ REQUEST_STATUS_TOOL,
5135
+ REQUEST_PAY_TOOL
4831
5136
  ]
4832
5137
  }));
4833
5138
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
@@ -4930,6 +5235,18 @@ async function main() {
4930
5235
  const parsed = YieldWithdrawInputSchema.parse(args ?? {});
4931
5236
  return await runYieldWithdraw(parsed);
4932
5237
  }
5238
+ case "q402_request_create": {
5239
+ const parsed = RequestCreateInputSchema.parse(args ?? {});
5240
+ return { content: [jsonText(await runRequestCreate(parsed))] };
5241
+ }
5242
+ case "q402_request_status": {
5243
+ const parsed = RequestStatusInputSchema.parse(args ?? {});
5244
+ return { content: [jsonText(await runRequestStatus(parsed))] };
5245
+ }
5246
+ case "q402_request_pay": {
5247
+ const parsed = RequestPayInputSchema.parse(args ?? {});
5248
+ return { content: [jsonText(await runRequestPay(parsed))] };
5249
+ }
4933
5250
  default:
4934
5251
  return {
4935
5252
  isError: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quackai/q402-mcp",
3
- "version": "0.8.31",
3
+ "version": "0.8.33",
4
4
  "description": "MCP server for Q402 — gasless USDC/USDT/RLUSD payments on 10 EVM chains + Chainlink CCIP USDC bridge on the eth/avax/arbitrum triangle, callable from Claude (Desktop / Code), OpenAI Codex CLI, and any other Model Context Protocol client.",
5
5
  "mcpName": "io.github.bitgett/q402-mcp",
6
6
  "keywords": [
@@ -68,6 +68,8 @@
68
68
  "access": "public"
69
69
  },
70
70
  "overrides": {
71
- "esbuild": "^0.28.1"
71
+ "esbuild": "^0.28.1",
72
+ "ws": "^8.21.0",
73
+ "hono": "^4.12.25"
72
74
  }
73
75
  }