@quackai/q402-mcp 0.1.3 → 0.2.1

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 +7 -1
  2. package/dist/index.js +170 -3
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -41,11 +41,17 @@ You'll get a ranked breakdown immediately — no API key, no signup, no funds at
41
41
  | Tool | Auth | Purpose |
42
42
  |---|---|---|
43
43
  | `q402_quote` | none | Compare gas cost and supported tokens across chains. Read-only. |
44
- | `q402_balance` | API key | Verify the API key, show tier (live vs sandbox), and remaining quota. |
44
+ | `q402_balance` | API key | Verify the API key and report its plan tier + remaining quota credits (live vs sandbox). |
45
45
  | `q402_pay` | API key + private key + flag | Send a gasless payment. **Sandbox by default** — see [Sandbox vs live mode](#sandbox-vs-live-mode). |
46
+ | `q402_receipt` | none | Look up a Trust Receipt by `rct_…` id and locally verify its ECDSA signature against the relayer EOA. Returns the public settlement record + a `verified` boolean. *receiptId-only today; tx-hash lookup reserved for a future release.* |
46
47
 
47
48
  `q402_pay` follows a "confirm in chat first" contract: the tool description instructs the model to never call it without explicit user approval of the recipient address, amount, chain, and token.
48
49
 
50
+ `q402_receipt` is the natural follow-up: after `q402_pay` returns a `receiptUrl`, hand the agent the `rct_…` id and ask *"verify this receipt"* — the tool re-runs the same canonical-JSON + EIP-191 recovery the receipt page does in the browser, so the verification doesn't depend on trusting any UI. Example prompts that work today:
51
+
52
+ > *"Pay 0.10 USDT on BNB to vitalik.eth, then verify the receipt."*
53
+ > *"Is `rct_afa5f50bc49a65ebba3b28ab` a real Q402 receipt? Verify the signature."*
54
+
49
55
  > Per-chain gas tank balances and full transaction history live in the [dashboard](https://q402.quackai.ai/dashboard) — those endpoints require a wallet signature, not a bare API key, so the MCP server points the agent there instead of exposing them.
50
56
 
51
57
  ---
package/dist/index.js CHANGED
@@ -561,7 +561,7 @@ async function runBalance() {
561
561
  }
562
562
  var BALANCE_TOOL = {
563
563
  name: "q402_balance",
564
- description: "Verify the configured API key and show its tier (live vs sandbox) and remaining subscription quota. Read-only. For per-chain gas tank balances, point the user at https://q402.quackai.ai/dashboard \u2014 that data needs a wallet signature, not a bare key.",
564
+ description: "Verify the configured API key and report its plan tier (live vs sandbox). Read-only. For remaining daily quota and per-chain gas tank balances, point the user at https://q402.quackai.ai/dashboard \u2014 those need a wallet signature, not a bare key.",
565
565
  inputSchema: {
566
566
  type: "object",
567
567
  properties: {},
@@ -569,9 +569,172 @@ var BALANCE_TOOL = {
569
569
  }
570
570
  };
571
571
 
572
+ // src/tools/receipt.ts
573
+ import { z as z4 } from "zod";
574
+ import { keccak256, toUtf8Bytes, getBytes, verifyMessage } from "ethers";
575
+ var ReceiptShape = z4.object({
576
+ receiptId: z4.string(),
577
+ createdAt: z4.string(),
578
+ txHash: z4.string(),
579
+ blockNumber: z4.number().optional(),
580
+ chain: z4.string(),
581
+ payer: z4.string(),
582
+ recipient: z4.string(),
583
+ token: z4.enum(["USDC", "USDT"]),
584
+ tokenAmount: z4.string(),
585
+ tokenAmountRaw: z4.string(),
586
+ method: z4.enum(["eip7702", "eip3009", "eip7702_xlayer", "eip7702_stable"]),
587
+ gasCostNative: z4.string().optional(),
588
+ apiKeyTier: z4.string().optional(),
589
+ // hidden by default in publicView
590
+ showTier: z4.boolean(),
591
+ sandbox: z4.boolean(),
592
+ webhook: z4.object({
593
+ configured: z4.boolean(),
594
+ event: z4.string(),
595
+ deliveryStatus: z4.enum(["pending", "delivered", "failed", "not_configured"]),
596
+ attempts: z4.number().optional(),
597
+ lastStatusCode: z4.number().optional(),
598
+ lastError: z4.string().optional(),
599
+ deliveredAt: z4.string().optional(),
600
+ payloadSha256: z4.string().optional(),
601
+ signatureSha256: z4.string().optional()
602
+ }),
603
+ signature: z4.string(),
604
+ signedBy: z4.string(),
605
+ signedAt: z4.string()
606
+ });
607
+ function canonicalize(fields) {
608
+ const sorted = {};
609
+ for (const k of Object.keys(fields).sort()) {
610
+ sorted[k] = fields[k];
611
+ }
612
+ return JSON.stringify(sorted);
613
+ }
614
+ function digest(canonical) {
615
+ return keccak256(toUtf8Bytes(canonical));
616
+ }
617
+ function verifyReceiptSignature(r) {
618
+ try {
619
+ const fields = {
620
+ receiptId: r.receiptId,
621
+ createdAt: r.createdAt,
622
+ txHash: r.txHash,
623
+ chain: r.chain,
624
+ payer: r.payer,
625
+ recipient: r.recipient,
626
+ token: r.token,
627
+ tokenAmount: r.tokenAmount,
628
+ tokenAmountRaw: r.tokenAmountRaw,
629
+ method: r.method,
630
+ sandbox: r.sandbox
631
+ };
632
+ const recovered = verifyMessage(getBytes(digest(canonicalize(fields))), r.signature).toLowerCase();
633
+ return recovered === r.signedBy.toLowerCase();
634
+ } catch {
635
+ return false;
636
+ }
637
+ }
638
+ var ReceiptInputSchema = z4.object({
639
+ receiptId: z4.string().regex(/^rct_[0-9a-f]{24}$/, "receiptId must match rct_<24-hex>").optional(),
640
+ txHash: z4.string().regex(/^0x[0-9a-fA-F]{64}$/, "txHash must be a 0x-prefixed 32-byte hex").optional()
641
+ }).refine((v) => !!v.receiptId || !!v.txHash, {
642
+ message: "Provide receiptId OR txHash (at least one)"
643
+ });
644
+ var EXPLORERS = {
645
+ bnb: (h) => `https://bscscan.com/tx/${h}`,
646
+ eth: (h) => `https://etherscan.io/tx/${h}`,
647
+ avax: (h) => `https://snowtrace.io/tx/${h}`,
648
+ xlayer: (h) => `https://www.oklink.com/xlayer/tx/${h}`,
649
+ stable: (h) => `https://stable-explorer.io/tx/${h}`,
650
+ mantle: (h) => `https://mantlescan.xyz/tx/${h}`,
651
+ injective: (h) => `https://blockscout.injective.network/tx/${h}`
652
+ };
653
+ function receiptApiBase() {
654
+ return CONFIG.relayBaseUrl;
655
+ }
656
+ function pageBase() {
657
+ return CONFIG.relayBaseUrl.replace(/\/api\/?$/, "");
658
+ }
659
+ async function runReceipt(input) {
660
+ const apiBase = receiptApiBase();
661
+ const receiptId = input.receiptId ?? null;
662
+ if (!receiptId && input.txHash) {
663
+ return {
664
+ receiptId: null,
665
+ url: null,
666
+ pageUrl: null,
667
+ verified: false,
668
+ explorerUrl: null,
669
+ receipt: null,
670
+ notFound: true
671
+ };
672
+ }
673
+ if (!receiptId) {
674
+ return {
675
+ receiptId: null,
676
+ url: null,
677
+ pageUrl: null,
678
+ verified: false,
679
+ explorerUrl: null,
680
+ receipt: null,
681
+ notFound: true
682
+ };
683
+ }
684
+ const resp = await fetch(`${apiBase}/receipt/${receiptId}`);
685
+ if (resp.status === 404) {
686
+ return {
687
+ receiptId,
688
+ url: null,
689
+ pageUrl: null,
690
+ verified: false,
691
+ explorerUrl: null,
692
+ receipt: null,
693
+ notFound: true
694
+ };
695
+ }
696
+ if (!resp.ok) {
697
+ throw new Error(`receipt fetch failed: HTTP ${resp.status}`);
698
+ }
699
+ const raw = await resp.json();
700
+ const receipt = ReceiptShape.parse(raw);
701
+ const pUrl = `${pageBase()}/receipt/${receipt.receiptId}`;
702
+ const explorer = EXPLORERS[receipt.chain];
703
+ const explorerUrl = explorer ? explorer(receipt.txHash) : null;
704
+ const verified = verifyReceiptSignature(receipt);
705
+ return {
706
+ receiptId: receipt.receiptId,
707
+ url: pUrl,
708
+ pageUrl: pUrl,
709
+ verified,
710
+ explorerUrl,
711
+ receipt
712
+ };
713
+ }
714
+ var RECEIPT_TOOL = {
715
+ name: "q402_receipt",
716
+ description: "Look up a Q402 Trust Receipt by its rct_\u2026 receiptId and return the settlement record + a locally-verified ECDSA boolean (the tool re-runs the same canonical-JSON + EIP-191 recovery the receipt page does in the browser). Read-only; no API key required. Use after q402_pay to give the user a shareable verified-by-Q402 URL, or to independently verify a receipt id someone shared with you. **receiptId is required**; passing only txHash returns notFound (tx \u2192 receiptId lookup is reserved for a future release).",
717
+ inputSchema: {
718
+ type: "object",
719
+ properties: {
720
+ receiptId: {
721
+ type: "string",
722
+ pattern: "^rct_[0-9a-f]{24}$",
723
+ description: "Receipt id (rct_ + 24 hex chars). Returned by q402_pay; also visible at the end of any /receipt/ URL. This is the only path that resolves today."
724
+ },
725
+ txHash: {
726
+ type: "string",
727
+ pattern: "^0x[0-9a-fA-F]{64}$",
728
+ description: "Reserved for a future tx \u2192 receipt index. Today this is unimplemented and the tool returns notFound when only txHash is provided. Pass receiptId instead."
729
+ }
730
+ },
731
+ additionalProperties: false
732
+ }
733
+ };
734
+
572
735
  // src/index.ts
573
736
  var PACKAGE_NAME = "@quackai/q402-mcp";
574
- var PACKAGE_VERSION = "0.1.3";
737
+ var PACKAGE_VERSION = "0.2.1";
575
738
  function jsonText(value) {
576
739
  return { type: "text", text: JSON.stringify(value, null, 2) };
577
740
  }
@@ -581,7 +744,7 @@ async function main() {
581
744
  { capabilities: { tools: {} } }
582
745
  );
583
746
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
584
- tools: [QUOTE_TOOL, BALANCE_TOOL, PAY_TOOL]
747
+ tools: [QUOTE_TOOL, BALANCE_TOOL, PAY_TOOL, RECEIPT_TOOL]
585
748
  }));
586
749
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
587
750
  const { name, arguments: args } = req.params;
@@ -599,6 +762,10 @@ async function main() {
599
762
  const parsed = PayInputSchema.parse(args ?? {});
600
763
  return { content: [jsonText(await runPay(parsed))] };
601
764
  }
765
+ case "q402_receipt": {
766
+ const parsed = ReceiptInputSchema.parse(args ?? {});
767
+ return { content: [jsonText(await runReceipt(parsed))] };
768
+ }
602
769
  default:
603
770
  return {
604
771
  isError: true,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@quackai/q402-mcp",
3
- "version": "0.1.3",
4
- "description": "MCP server for Q402 — gasless stablecoin payments across 7 EVM chains, callable directly from Claude Desktop and any MCP-compatible AI agent.",
3
+ "version": "0.2.1",
4
+ "description": "MCP server for Q402 — gasless USDC and USDT payments across 7 EVM chains, callable directly from Claude Desktop and any other Model Context Protocol client.",
5
5
  "mcpName": "io.github.bitgett/q402-mcp",
6
6
  "keywords": [
7
7
  "mcp",