@quackai/q402-mcp 0.3.7 → 0.3.9
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 +5 -4
- package/dist/index.js +296 -37
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@quackai/q402-mcp)
|
|
6
6
|
[](./LICENSE)
|
|
7
7
|
|
|
8
|
-
> **🎟️ Free trial available (2026-05-
|
|
8
|
+
> **🎟️ Free trial available (2026-05-19 → 2026-06-30)** — 2,000 gasless transactions on BNB Chain (USDC + USDT), 30-day window, no card. One wallet signature: <https://q402.quackai.ai>.
|
|
9
9
|
>
|
|
10
|
-
> **
|
|
10
|
+
> **Trial-scope policy:** API keys minted under the free-trial program (`plan: "trial"`) are restricted to BNB Chain with USDC/USDT — server-side enforcement, returns `403 TRIAL_BNB_ONLY` otherwise. **Paid API keys see the full 7-chain matrix at all times.**
|
|
11
11
|
|
|
12
12
|
Claude can now reason about stablecoin payments end to end — quote a transfer across 7 chains, pick the cheapest route, and (optionally) settle the transaction over [Q402](https://q402.quackai.ai)'s EIP-7702 relayer infrastructure. The recipient receives the full amount; the sender pays $0 in gas.
|
|
13
13
|
|
|
@@ -46,10 +46,11 @@ You'll get a ranked breakdown immediately — no API key, no signup, no funds at
|
|
|
46
46
|
|---|---|---|
|
|
47
47
|
| `q402_quote` | none | Compare gas cost and supported tokens across chains. Read-only. |
|
|
48
48
|
| `q402_balance` | API key | Verify the API key and report its plan tier + remaining quota credits (live vs sandbox). |
|
|
49
|
-
| `q402_pay` | API key + private key + flag | Send a gasless payment. **Sandbox by default** — see [Sandbox vs live mode](#sandbox-vs-live-mode). |
|
|
49
|
+
| `q402_pay` | API key + private key + flag | Send a gasless payment to a single recipient. **Sandbox by default** — see [Sandbox vs live mode](#sandbox-vs-live-mode). |
|
|
50
|
+
| `q402_batch_pay` | API key + private key + flag | Send a gasless payment to **multiple** recipients in one call on a single chain × token. Trial keys: 5 rows max. Paid keys: 20 rows max. Same sandbox gating as `q402_pay`. |
|
|
50
51
|
| `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.* |
|
|
51
52
|
|
|
52
|
-
`q402_pay`
|
|
53
|
+
`q402_pay` and `q402_batch_pay` follow a "confirm in chat first" contract: the tool description instructs the model to never call it without explicit user approval of the recipient address(es), amount(s), chain, and token. For batch calls the user must approve the **full batch**, not the individual rows.
|
|
53
54
|
|
|
54
55
|
`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:
|
|
55
56
|
|
package/dist/index.js
CHANGED
|
@@ -431,6 +431,116 @@ var Q402NodeClient = class _Q402NodeClient {
|
|
|
431
431
|
data.explorerUrl = _Q402NodeClient.explorerUrl(chain, data.txHash);
|
|
432
432
|
return data;
|
|
433
433
|
}
|
|
434
|
+
/**
|
|
435
|
+
* Multi-recipient settlement on a single chain + token. Trial keys can
|
|
436
|
+
* fan out to at most 5 recipients per call; paid keys up to 20. The
|
|
437
|
+
* server enforces the cap and rejects oversized batches with
|
|
438
|
+
* `BATCH_TOO_LARGE`.
|
|
439
|
+
*
|
|
440
|
+
* Each recipient is independently authorised: one EIP-712
|
|
441
|
+
* TransferAuthorization witness + one EIP-7702 authorization tuple
|
|
442
|
+
* per row. The authorization nonces are issued sequentially starting
|
|
443
|
+
* from the EOA's current on-chain nonce, so the EVM applies them
|
|
444
|
+
* cleanly in batch order. Execution is sequential server-side; the
|
|
445
|
+
* first transfer must succeed (it installs / re-confirms the
|
|
446
|
+
* delegation), after which the remaining transfers are surfaced in
|
|
447
|
+
* the result array even if individual ones fail.
|
|
448
|
+
*/
|
|
449
|
+
async batchPay(inputs) {
|
|
450
|
+
if (!Array.isArray(inputs) || inputs.length === 0) {
|
|
451
|
+
throw new Error("batchPay requires at least one recipient");
|
|
452
|
+
}
|
|
453
|
+
const { chain, relayBaseUrl, apiKey, privateKey } = this.opts;
|
|
454
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
455
|
+
const inp = inputs[i];
|
|
456
|
+
const tokenCfg = tokenFor(chain, inp.token);
|
|
457
|
+
if (chain.supportedTokens && !chain.supportedTokens.includes(inp.token)) {
|
|
458
|
+
throw new Error(
|
|
459
|
+
`recipient[${i}]: token ${inp.token} is not supported on chain ${chain.key}. Supported: ${chain.supportedTokens.join(", ")}.`
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
try {
|
|
463
|
+
toRawAmount(inp.amount, tokenCfg.decimals);
|
|
464
|
+
} catch (e) {
|
|
465
|
+
throw new Error(
|
|
466
|
+
`recipient[${i}]: ${e instanceof Error ? e.message : String(e)}`
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const rpcUrl = this.opts.rpcUrl ?? DEFAULT_RPC[chain.chainId];
|
|
471
|
+
if (!rpcUrl) {
|
|
472
|
+
throw new Error(
|
|
473
|
+
`no RPC URL configured for chain ${chain.key} (chainId ${chain.chainId})`
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
const provider = new JsonRpcProvider(rpcUrl);
|
|
477
|
+
const wallet = new Wallet(privateKey, provider);
|
|
478
|
+
const owner = await wallet.getAddress();
|
|
479
|
+
const facilitator = await this.fetchFacilitator();
|
|
480
|
+
const baseAuthNonce = await provider.getTransactionCount(owner);
|
|
481
|
+
const deadline = Math.floor(Date.now() / 1e3) + 600;
|
|
482
|
+
const recipients = [];
|
|
483
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
484
|
+
const inp = inputs[i];
|
|
485
|
+
const tokenCfg = tokenFor(chain, inp.token);
|
|
486
|
+
const amountRaw = toRawAmount(inp.amount, tokenCfg.decimals);
|
|
487
|
+
const paymentNonce = toBigInt(randomBytes(32));
|
|
488
|
+
const witnessSig = await wallet.signTypedData(
|
|
489
|
+
{
|
|
490
|
+
name: chain.domainName,
|
|
491
|
+
version: "1",
|
|
492
|
+
chainId: chain.chainId,
|
|
493
|
+
verifyingContract: owner
|
|
494
|
+
},
|
|
495
|
+
TRANSFER_AUTH_TYPES,
|
|
496
|
+
{
|
|
497
|
+
owner,
|
|
498
|
+
facilitator,
|
|
499
|
+
token: tokenCfg.address,
|
|
500
|
+
recipient: inp.to,
|
|
501
|
+
amount: BigInt(amountRaw),
|
|
502
|
+
nonce: paymentNonce,
|
|
503
|
+
deadline: BigInt(deadline)
|
|
504
|
+
}
|
|
505
|
+
);
|
|
506
|
+
const authorization = await signAuthorization(wallet, {
|
|
507
|
+
chainId: chain.chainId,
|
|
508
|
+
address: chain.implContract,
|
|
509
|
+
nonce: baseAuthNonce + i
|
|
510
|
+
});
|
|
511
|
+
const baseRow = {
|
|
512
|
+
from: owner,
|
|
513
|
+
to: inp.to,
|
|
514
|
+
amount: amountRaw,
|
|
515
|
+
deadline,
|
|
516
|
+
witnessSig,
|
|
517
|
+
authorization
|
|
518
|
+
};
|
|
519
|
+
const row = chain.key === "xlayer" ? { ...baseRow, xlayerNonce: paymentNonce.toString() } : chain.key === "stable" ? { ...baseRow, stableNonce: paymentNonce.toString() } : { ...baseRow, nonce: paymentNonce.toString() };
|
|
520
|
+
recipients.push(row);
|
|
521
|
+
}
|
|
522
|
+
const resp = await fetch(`${relayBaseUrl.replace(/\/$/, "")}/relay/batch`, {
|
|
523
|
+
method: "POST",
|
|
524
|
+
headers: { "Content-Type": "application/json" },
|
|
525
|
+
body: JSON.stringify({
|
|
526
|
+
apiKey,
|
|
527
|
+
chain: chain.key,
|
|
528
|
+
token: inputs[0].token,
|
|
529
|
+
// all recipients must share token; pre-validated above
|
|
530
|
+
facilitator,
|
|
531
|
+
recipients
|
|
532
|
+
})
|
|
533
|
+
});
|
|
534
|
+
const data = await resp.json();
|
|
535
|
+
if (!resp.ok) {
|
|
536
|
+
throw new Error(data.error ?? `relay/batch failed (HTTP ${resp.status})`);
|
|
537
|
+
}
|
|
538
|
+
data.results = data.results.map((r) => ({
|
|
539
|
+
...r,
|
|
540
|
+
...r.success && r.txHash ? { explorerUrl: _Q402NodeClient.explorerUrl(chain, r.txHash) } : {}
|
|
541
|
+
}));
|
|
542
|
+
return data;
|
|
543
|
+
}
|
|
434
544
|
};
|
|
435
545
|
function sandboxPay(chain, input) {
|
|
436
546
|
const tokenCfg = tokenFor(chain, input.token);
|
|
@@ -561,9 +671,154 @@ var PAY_TOOL = {
|
|
|
561
671
|
}
|
|
562
672
|
};
|
|
563
673
|
|
|
564
|
-
// src/tools/
|
|
674
|
+
// src/tools/batch-pay.ts
|
|
675
|
+
import { isAddress as isAddress3 } from "ethers";
|
|
565
676
|
import { z as z3 } from "zod";
|
|
566
|
-
var
|
|
677
|
+
var RECIPIENT_LIMIT_TRIAL = 5;
|
|
678
|
+
var RECIPIENT_LIMIT_PAID = 20;
|
|
679
|
+
var CLIENT_RECIPIENT_CAP = RECIPIENT_LIMIT_PAID;
|
|
680
|
+
var BatchPayInputSchema = z3.object({
|
|
681
|
+
chain: z3.enum(["avax", "bnb", "eth", "xlayer", "stable", "mantle", "injective"]),
|
|
682
|
+
token: z3.enum(["USDC", "USDT", "RLUSD"]).describe(
|
|
683
|
+
"Stablecoin symbol. USDC / USDT supported on most chains (Injective is USDT-only). RLUSD (Ripple USD, NY DFS regulated, decimals 18) is Ethereum-only. The same token applies to every recipient in the batch."
|
|
684
|
+
),
|
|
685
|
+
recipients: z3.array(
|
|
686
|
+
z3.object({
|
|
687
|
+
to: z3.string().refine(isAddress3, "to must be a valid 0x-prefixed EVM address").describe("Recipient EVM address (0x + 40 hex)."),
|
|
688
|
+
amount: z3.string().regex(/^\d+(\.\d+)?$/, "amount must be a positive decimal string").describe('Human-readable decimal amount for this recipient, e.g. "5.00".')
|
|
689
|
+
})
|
|
690
|
+
).min(1, "recipients must contain at least one row").max(CLIENT_RECIPIENT_CAP, `recipients cannot exceed ${CLIENT_RECIPIENT_CAP} (server enforces tighter cap by key scope)`).describe(
|
|
691
|
+
`Array of {to, amount} pairs. All recipients share the same chain and token. Trial keys: max ${RECIPIENT_LIMIT_TRIAL} rows. Paid keys: max ${RECIPIENT_LIMIT_PAID} rows.`
|
|
692
|
+
),
|
|
693
|
+
confirm: z3.literal(true).describe(
|
|
694
|
+
"MUST be true. The user must have explicitly approved this exact set of recipients, amounts, chain, and token in the conversation right before this tool was called. Setting confirm=true on behalf of the user without that approval is a violation of the tool contract."
|
|
695
|
+
)
|
|
696
|
+
});
|
|
697
|
+
function maxAmountGuardBatch(recipients, cap) {
|
|
698
|
+
for (let i = 0; i < recipients.length; i++) {
|
|
699
|
+
const r = recipients[i];
|
|
700
|
+
const numeric = Number(r.amount);
|
|
701
|
+
if (!Number.isFinite(numeric)) {
|
|
702
|
+
throw new Error(`recipients[${i}]: unparseable amount "${r.amount}"`);
|
|
703
|
+
}
|
|
704
|
+
if (numeric > cap) {
|
|
705
|
+
throw new Error(
|
|
706
|
+
`recipients[${i}]: amount $${r.amount} exceeds the per-call cap of $${cap}. Set Q402_MAX_AMOUNT_PER_CALL to a higher value if intentional.`
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
function recipientAllowlistGuardBatch(recipients, allow) {
|
|
712
|
+
if (allow.length === 0) return;
|
|
713
|
+
for (let i = 0; i < recipients.length; i++) {
|
|
714
|
+
const to = recipients[i].to.toLowerCase();
|
|
715
|
+
if (!allow.includes(to)) {
|
|
716
|
+
throw new Error(
|
|
717
|
+
`recipients[${i}]: ${recipients[i].to} is not in Q402_ALLOWED_RECIPIENTS. Either add this address to the allowlist or unset the env var to disable the guard.`
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
async function runBatchPay(input) {
|
|
723
|
+
const chain = getChain(input.chain);
|
|
724
|
+
tokenFor(chain, input.token);
|
|
725
|
+
if (chain.supportedTokens && !chain.supportedTokens.includes(input.token)) {
|
|
726
|
+
throw new Error(
|
|
727
|
+
`token ${input.token} is not supported on chain ${chain.key}. Supported on this chain: ${chain.supportedTokens.join(", ")}.`
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
const guardsApplied = [];
|
|
731
|
+
maxAmountGuardBatch(input.recipients, CONFIG.maxAmountPerCallUsd);
|
|
732
|
+
guardsApplied.push(`max_amount<=${CONFIG.maxAmountPerCallUsd} (per recipient)`);
|
|
733
|
+
recipientAllowlistGuardBatch(input.recipients, CONFIG.allowedRecipients);
|
|
734
|
+
if (CONFIG.allowedRecipients.length > 0) {
|
|
735
|
+
guardsApplied.push(`recipient_allowlist[${CONFIG.allowedRecipients.length}]`);
|
|
736
|
+
}
|
|
737
|
+
if (CONFIG.mode === "sandbox") {
|
|
738
|
+
const sandboxResults = input.recipients.map(
|
|
739
|
+
(r) => sandboxPay(chain, { to: r.to, amount: r.amount, token: input.token })
|
|
740
|
+
);
|
|
741
|
+
guardsApplied.push("mode=sandbox");
|
|
742
|
+
return {
|
|
743
|
+
mode: "sandbox",
|
|
744
|
+
result: { sandbox: sandboxResults, reason: describeSandboxReason2() },
|
|
745
|
+
guardsApplied,
|
|
746
|
+
setupHint: describeSandboxReason2()
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
const client = new Q402NodeClient({
|
|
750
|
+
apiKey: CONFIG.apiKey,
|
|
751
|
+
privateKey: CONFIG.privateKey,
|
|
752
|
+
chain,
|
|
753
|
+
relayBaseUrl: CONFIG.relayBaseUrl
|
|
754
|
+
});
|
|
755
|
+
const result = await client.batchPay(
|
|
756
|
+
input.recipients.map((r) => ({ to: r.to, amount: r.amount, token: input.token }))
|
|
757
|
+
);
|
|
758
|
+
guardsApplied.push("mode=live");
|
|
759
|
+
guardsApplied.push(`scope=${result.scope} (server enforced)`);
|
|
760
|
+
guardsApplied.push(`batch_size=${input.recipients.length}/${result.limit}`);
|
|
761
|
+
return { mode: "live", result, guardsApplied };
|
|
762
|
+
}
|
|
763
|
+
function describeSandboxReason2() {
|
|
764
|
+
const missing = [];
|
|
765
|
+
if (CONFIG.apiKeyKind !== "live") missing.push("Q402_API_KEY (must start with q402_live_)");
|
|
766
|
+
if (!CONFIG.privateKey) missing.push("Q402_PRIVATE_KEY");
|
|
767
|
+
if (!CONFIG.realPaymentsRequested) missing.push("Q402_ENABLE_REAL_PAYMENTS=1");
|
|
768
|
+
if (missing.length === 0) return "Sandbox mode active (no env state change needed).";
|
|
769
|
+
return "Sandbox mode is active because the following env vars are missing or not yet set: " + missing.join(", ") + ". Get a live API key at https://q402.quackai.ai/dashboard.";
|
|
770
|
+
}
|
|
771
|
+
var BATCH_PAY_TOOL = {
|
|
772
|
+
name: "q402_batch_pay",
|
|
773
|
+
description: `Send gasless payments to MULTIPLE recipients on a single chain \xD7 token in one call. Trial keys (q402_live_* with plan='trial'): max ${RECIPIENT_LIMIT_TRIAL} recipients per call, BNB Chain + USDC/USDT only. Paid keys: max ${RECIPIENT_LIMIT_PAID} recipients per call, full 7-chain support. SANDBOX BY DEFAULT \u2014 real on-chain TX only when Q402_API_KEY (live), Q402_PRIVATE_KEY, and Q402_ENABLE_REAL_PAYMENTS=1 are all set. Every recipient receives the full amount; the sender pays $0 in gas for the entire batch. ALWAYS get explicit user confirmation of the complete recipient + amount list, chain, and token in conversation immediately before calling this tool \u2014 the user must approve the full batch, not the individual rows.`,
|
|
774
|
+
inputSchema: {
|
|
775
|
+
type: "object",
|
|
776
|
+
properties: {
|
|
777
|
+
chain: {
|
|
778
|
+
type: "string",
|
|
779
|
+
enum: CHAIN_KEYS,
|
|
780
|
+
description: "Target chain. Applies to every recipient in the batch."
|
|
781
|
+
},
|
|
782
|
+
token: {
|
|
783
|
+
type: "string",
|
|
784
|
+
enum: ["USDC", "USDT", "RLUSD"],
|
|
785
|
+
description: "Stablecoin for the entire batch. USDC / USDT supported on most chains; Injective is USDT-only; RLUSD (decimals 18) is Ethereum-only."
|
|
786
|
+
},
|
|
787
|
+
recipients: {
|
|
788
|
+
type: "array",
|
|
789
|
+
minItems: 1,
|
|
790
|
+
maxItems: CLIENT_RECIPIENT_CAP,
|
|
791
|
+
description: "List of recipients. Trial keys: max 5. Paid keys: max 20. Each item is {to, amount}.",
|
|
792
|
+
items: {
|
|
793
|
+
type: "object",
|
|
794
|
+
properties: {
|
|
795
|
+
to: {
|
|
796
|
+
type: "string",
|
|
797
|
+
description: "Recipient EVM address (0x + 40 hex)."
|
|
798
|
+
},
|
|
799
|
+
amount: {
|
|
800
|
+
type: "string",
|
|
801
|
+
description: 'Human-readable decimal amount for this recipient, e.g. "5.00".'
|
|
802
|
+
}
|
|
803
|
+
},
|
|
804
|
+
required: ["to", "amount"],
|
|
805
|
+
additionalProperties: false
|
|
806
|
+
}
|
|
807
|
+
},
|
|
808
|
+
confirm: {
|
|
809
|
+
type: "boolean",
|
|
810
|
+
const: true,
|
|
811
|
+
description: "MUST be true and only set after the user has confirmed the entire batch in chat."
|
|
812
|
+
}
|
|
813
|
+
},
|
|
814
|
+
required: ["chain", "token", "recipients", "confirm"],
|
|
815
|
+
additionalProperties: false
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
// src/tools/balance.ts
|
|
820
|
+
import { z as z4 } from "zod";
|
|
821
|
+
var BalanceInputSchema = z4.object({});
|
|
567
822
|
function mask(key) {
|
|
568
823
|
if (!key || key.length < 12) return null;
|
|
569
824
|
return `${key.slice(0, 12)}\u2026${key.slice(-4)}`;
|
|
@@ -609,39 +864,39 @@ var BALANCE_TOOL = {
|
|
|
609
864
|
};
|
|
610
865
|
|
|
611
866
|
// src/tools/receipt.ts
|
|
612
|
-
import { z as
|
|
867
|
+
import { z as z5 } from "zod";
|
|
613
868
|
import { keccak256, toUtf8Bytes, getBytes, verifyMessage } from "ethers";
|
|
614
|
-
var ReceiptShape =
|
|
615
|
-
receiptId:
|
|
616
|
-
createdAt:
|
|
617
|
-
txHash:
|
|
618
|
-
blockNumber:
|
|
619
|
-
chain:
|
|
620
|
-
payer:
|
|
621
|
-
recipient:
|
|
622
|
-
token:
|
|
623
|
-
tokenAmount:
|
|
624
|
-
tokenAmountRaw:
|
|
625
|
-
method:
|
|
626
|
-
gasCostNative:
|
|
627
|
-
apiKeyTier:
|
|
869
|
+
var ReceiptShape = z5.object({
|
|
870
|
+
receiptId: z5.string(),
|
|
871
|
+
createdAt: z5.string(),
|
|
872
|
+
txHash: z5.string(),
|
|
873
|
+
blockNumber: z5.number().optional(),
|
|
874
|
+
chain: z5.string(),
|
|
875
|
+
payer: z5.string(),
|
|
876
|
+
recipient: z5.string(),
|
|
877
|
+
token: z5.enum(["USDC", "USDT", "RLUSD"]),
|
|
878
|
+
tokenAmount: z5.string(),
|
|
879
|
+
tokenAmountRaw: z5.string(),
|
|
880
|
+
method: z5.enum(["eip7702", "eip3009", "eip7702_xlayer", "eip7702_stable"]),
|
|
881
|
+
gasCostNative: z5.string().optional(),
|
|
882
|
+
apiKeyTier: z5.string().optional(),
|
|
628
883
|
// hidden by default in publicView
|
|
629
|
-
showTier:
|
|
630
|
-
sandbox:
|
|
631
|
-
webhook:
|
|
632
|
-
configured:
|
|
633
|
-
event:
|
|
634
|
-
deliveryStatus:
|
|
635
|
-
attempts:
|
|
636
|
-
lastStatusCode:
|
|
637
|
-
lastError:
|
|
638
|
-
deliveredAt:
|
|
639
|
-
payloadSha256:
|
|
640
|
-
signatureSha256:
|
|
884
|
+
showTier: z5.boolean(),
|
|
885
|
+
sandbox: z5.boolean(),
|
|
886
|
+
webhook: z5.object({
|
|
887
|
+
configured: z5.boolean(),
|
|
888
|
+
event: z5.string(),
|
|
889
|
+
deliveryStatus: z5.enum(["pending", "delivered", "failed", "not_configured"]),
|
|
890
|
+
attempts: z5.number().optional(),
|
|
891
|
+
lastStatusCode: z5.number().optional(),
|
|
892
|
+
lastError: z5.string().optional(),
|
|
893
|
+
deliveredAt: z5.string().optional(),
|
|
894
|
+
payloadSha256: z5.string().optional(),
|
|
895
|
+
signatureSha256: z5.string().optional()
|
|
641
896
|
}),
|
|
642
|
-
signature:
|
|
643
|
-
signedBy:
|
|
644
|
-
signedAt:
|
|
897
|
+
signature: z5.string(),
|
|
898
|
+
signedBy: z5.string(),
|
|
899
|
+
signedAt: z5.string()
|
|
645
900
|
});
|
|
646
901
|
function canonicalize(fields) {
|
|
647
902
|
const sorted = {};
|
|
@@ -674,9 +929,9 @@ function verifyReceiptSignature(r) {
|
|
|
674
929
|
return false;
|
|
675
930
|
}
|
|
676
931
|
}
|
|
677
|
-
var ReceiptInputSchema =
|
|
678
|
-
receiptId:
|
|
679
|
-
txHash:
|
|
932
|
+
var ReceiptInputSchema = z5.object({
|
|
933
|
+
receiptId: z5.string().regex(/^rct_[0-9a-f]{24}$/, "receiptId must match rct_<24-hex>").optional(),
|
|
934
|
+
txHash: z5.string().regex(/^0x[0-9a-fA-F]{64}$/, "txHash must be a 0x-prefixed 32-byte hex").optional()
|
|
680
935
|
}).refine((v) => !!v.receiptId || !!v.txHash, {
|
|
681
936
|
message: "Provide receiptId OR txHash (at least one)"
|
|
682
937
|
});
|
|
@@ -773,7 +1028,7 @@ var RECEIPT_TOOL = {
|
|
|
773
1028
|
|
|
774
1029
|
// src/index.ts
|
|
775
1030
|
var PACKAGE_NAME = "@quackai/q402-mcp";
|
|
776
|
-
var PACKAGE_VERSION = "0.3.
|
|
1031
|
+
var PACKAGE_VERSION = "0.3.9";
|
|
777
1032
|
function jsonText(value) {
|
|
778
1033
|
return { type: "text", text: JSON.stringify(value, null, 2) };
|
|
779
1034
|
}
|
|
@@ -783,7 +1038,7 @@ async function main() {
|
|
|
783
1038
|
{ capabilities: { tools: {} } }
|
|
784
1039
|
);
|
|
785
1040
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
786
|
-
tools: [QUOTE_TOOL, BALANCE_TOOL, PAY_TOOL, RECEIPT_TOOL]
|
|
1041
|
+
tools: [QUOTE_TOOL, BALANCE_TOOL, PAY_TOOL, BATCH_PAY_TOOL, RECEIPT_TOOL]
|
|
787
1042
|
}));
|
|
788
1043
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
789
1044
|
const { name, arguments: args } = req.params;
|
|
@@ -801,6 +1056,10 @@ async function main() {
|
|
|
801
1056
|
const parsed = PayInputSchema.parse(args ?? {});
|
|
802
1057
|
return { content: [jsonText(await runPay(parsed))] };
|
|
803
1058
|
}
|
|
1059
|
+
case "q402_batch_pay": {
|
|
1060
|
+
const parsed = BatchPayInputSchema.parse(args ?? {});
|
|
1061
|
+
return { content: [jsonText(await runBatchPay(parsed))] };
|
|
1062
|
+
}
|
|
804
1063
|
case "q402_receipt": {
|
|
805
1064
|
const parsed = ReceiptInputSchema.parse(args ?? {});
|
|
806
1065
|
return { content: [jsonText(await runReceipt(parsed))] };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quackai/q402-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"description": "MCP server for Q402 — gasless USDC, USDT, and RLUSD 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": [
|