@quackai/q402-mcp 0.3.10 → 0.3.11
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 +14 -3
- package/dist/index.js +105 -39
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,13 +42,24 @@ Restart Claude Desktop and ask:
|
|
|
42
42
|
|
|
43
43
|
### OpenAI Codex CLI
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
Three install paths — pick the one that matches your workflow.
|
|
46
|
+
|
|
47
|
+
**(a) Codex plugin marketplace** (recommended — bundles the MCP config so users don't write TOML):
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
codex plugin marketplace add bitgett/q402-mcp
|
|
51
|
+
codex /plugins # browse and install "q402"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This repo carries a Codex plugin manifest at [`.codex-plugin/plugin.json`](./.codex-plugin/plugin.json) and a marketplace catalog at [`.agents/plugins/marketplace.json`](./.agents/plugins/marketplace.json), so any signed-in Codex user can register it as a marketplace source and install with one click.
|
|
55
|
+
|
|
56
|
+
**(b) Single MCP server via `codex mcp add`** (no plugin wrapper — just register the stdio server):
|
|
46
57
|
|
|
47
58
|
```bash
|
|
48
59
|
codex mcp add q402 -- npx -y @quackai/q402-mcp
|
|
49
60
|
```
|
|
50
61
|
|
|
51
|
-
|
|
62
|
+
**(c) Direct `~/.codex/config.toml` edit** (`.codex/config.toml` for per-project scope):
|
|
52
63
|
|
|
53
64
|
```toml
|
|
54
65
|
[mcp_servers.q402]
|
|
@@ -89,7 +100,7 @@ The server has no client-specific code. If your client speaks stdio MCP, point i
|
|
|
89
100
|
| `q402_quote` | none | Compare gas cost and supported tokens across chains. Read-only. |
|
|
90
101
|
| `q402_balance` | API key | Verify the API key and report its plan tier + remaining quota credits (live vs sandbox). |
|
|
91
102
|
| `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). |
|
|
92
|
-
| `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`. |
|
|
103
|
+
| `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. **Supported chains: avax, bnb, eth, mantle, injective** (default EIP-7702 mode). xlayer + stable are NOT batchable — use `q402_pay` in a loop for those. Same sandbox gating as `q402_pay`. |
|
|
93
104
|
| `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.* |
|
|
94
105
|
|
|
95
106
|
`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.
|
package/dist/index.js
CHANGED
|
@@ -445,28 +445,45 @@ var Q402NodeClient = class _Q402NodeClient {
|
|
|
445
445
|
* first transfer must succeed (it installs / re-confirms the
|
|
446
446
|
* delegation), after which the remaining transfers are surfaced in
|
|
447
447
|
* the result array even if individual ones fail.
|
|
448
|
+
*
|
|
449
|
+
* Signature shape: `{ token, recipients }`. The previous revision took
|
|
450
|
+
* `PayInput[]` (with token on each row), which read as if rows could
|
|
451
|
+
* carry different tokens — but the request body only ships one token
|
|
452
|
+
* field, so the per-row token on rows 1..N was silently ignored.
|
|
453
|
+
* Codex audit P2: surface the constraint in the type so consumers
|
|
454
|
+
* can't accidentally build a "mixed-token batch" that quietly drops
|
|
455
|
+
* the second token. Same chain + same token across one batch, full
|
|
456
|
+
* stop.
|
|
448
457
|
*/
|
|
449
|
-
async batchPay(
|
|
450
|
-
|
|
458
|
+
async batchPay(input) {
|
|
459
|
+
const { token, recipients: rows } = input;
|
|
460
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
451
461
|
throw new Error("batchPay requires at least one recipient");
|
|
452
462
|
}
|
|
463
|
+
if (typeof token !== "string") {
|
|
464
|
+
throw new Error("batchPay({ token, recipients }): token must be a string");
|
|
465
|
+
}
|
|
453
466
|
const { chain, relayBaseUrl, apiKey, privateKey } = this.opts;
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
}
|
|
467
|
+
const tokenCfg = tokenFor(chain, token);
|
|
468
|
+
if (chain.supportedTokens && !chain.supportedTokens.includes(token)) {
|
|
469
|
+
throw new Error(
|
|
470
|
+
`token ${token} is not supported on chain ${chain.key}. Supported: ${chain.supportedTokens.join(", ")}.`
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
for (let i = 0; i < rows.length; i++) {
|
|
462
474
|
try {
|
|
463
|
-
toRawAmount(
|
|
475
|
+
toRawAmount(rows[i].amount, tokenCfg.decimals);
|
|
464
476
|
} catch (e) {
|
|
465
477
|
throw new Error(
|
|
466
478
|
`recipient[${i}]: ${e instanceof Error ? e.message : String(e)}`
|
|
467
479
|
);
|
|
468
480
|
}
|
|
469
481
|
}
|
|
482
|
+
if (chain.key === "xlayer" || chain.key === "stable") {
|
|
483
|
+
throw new Error(
|
|
484
|
+
`batchPay does not yet support chain "${chain.key}". Supported batch chains: avax, bnb, eth, mantle, injective (default EIP-7702 mode). For "${chain.key}" use pay() in a client-side loop.`
|
|
485
|
+
);
|
|
486
|
+
}
|
|
470
487
|
const rpcUrl = this.opts.rpcUrl ?? DEFAULT_RPC[chain.chainId];
|
|
471
488
|
if (!rpcUrl) {
|
|
472
489
|
throw new Error(
|
|
@@ -479,11 +496,10 @@ var Q402NodeClient = class _Q402NodeClient {
|
|
|
479
496
|
const facilitator = await this.fetchFacilitator();
|
|
480
497
|
const baseAuthNonce = await provider.getTransactionCount(owner);
|
|
481
498
|
const deadline = Math.floor(Date.now() / 1e3) + 600;
|
|
482
|
-
const
|
|
483
|
-
for (let i = 0; i <
|
|
484
|
-
const
|
|
485
|
-
const
|
|
486
|
-
const amountRaw = toRawAmount(inp.amount, tokenCfg.decimals);
|
|
499
|
+
const signedRows = [];
|
|
500
|
+
for (let i = 0; i < rows.length; i++) {
|
|
501
|
+
const row = rows[i];
|
|
502
|
+
const amountRaw = toRawAmount(row.amount, tokenCfg.decimals);
|
|
487
503
|
const paymentNonce = toBigInt(randomBytes(32));
|
|
488
504
|
const witnessSig = await wallet.signTypedData(
|
|
489
505
|
{
|
|
@@ -497,7 +513,7 @@ var Q402NodeClient = class _Q402NodeClient {
|
|
|
497
513
|
owner,
|
|
498
514
|
facilitator,
|
|
499
515
|
token: tokenCfg.address,
|
|
500
|
-
recipient:
|
|
516
|
+
recipient: row.to,
|
|
501
517
|
amount: BigInt(amountRaw),
|
|
502
518
|
nonce: paymentNonce,
|
|
503
519
|
deadline: BigInt(deadline)
|
|
@@ -508,16 +524,15 @@ var Q402NodeClient = class _Q402NodeClient {
|
|
|
508
524
|
address: chain.implContract,
|
|
509
525
|
nonce: baseAuthNonce + i
|
|
510
526
|
});
|
|
511
|
-
|
|
527
|
+
signedRows.push({
|
|
512
528
|
from: owner,
|
|
513
|
-
to:
|
|
529
|
+
to: row.to,
|
|
514
530
|
amount: amountRaw,
|
|
531
|
+
nonce: paymentNonce.toString(),
|
|
515
532
|
deadline,
|
|
516
533
|
witnessSig,
|
|
517
534
|
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);
|
|
535
|
+
});
|
|
521
536
|
}
|
|
522
537
|
const resp = await fetch(`${relayBaseUrl.replace(/\/$/, "")}/relay/batch`, {
|
|
523
538
|
method: "POST",
|
|
@@ -525,15 +540,23 @@ var Q402NodeClient = class _Q402NodeClient {
|
|
|
525
540
|
body: JSON.stringify({
|
|
526
541
|
apiKey,
|
|
527
542
|
chain: chain.key,
|
|
528
|
-
token
|
|
529
|
-
// all recipients must share token; pre-validated above
|
|
543
|
+
token,
|
|
530
544
|
facilitator,
|
|
531
|
-
recipients
|
|
545
|
+
recipients: signedRows
|
|
532
546
|
})
|
|
533
547
|
});
|
|
534
548
|
const data = await resp.json();
|
|
535
|
-
if (!resp.ok) {
|
|
536
|
-
|
|
549
|
+
if (!resp.ok || data.ok === false) {
|
|
550
|
+
const err = new BatchPayError(
|
|
551
|
+
data.aborted ? `Batch aborted: recipient[0] failed (${data.results?.[0]?.error ?? "unknown"}). No transfers landed.` : data.totalFailed > 0 ? `Batch completed with ${data.totalFailed}/${data.results?.length ?? "?"} failed rows.` : data.error ?? `relay/batch failed (HTTP ${resp.status})`,
|
|
552
|
+
{
|
|
553
|
+
aborted: !!data.aborted,
|
|
554
|
+
totalSuccess: data.totalSuccess ?? 0,
|
|
555
|
+
totalFailed: data.totalFailed ?? signedRows.length,
|
|
556
|
+
results: data.results ?? []
|
|
557
|
+
}
|
|
558
|
+
);
|
|
559
|
+
throw err;
|
|
537
560
|
}
|
|
538
561
|
data.results = data.results.map((r) => ({
|
|
539
562
|
...r,
|
|
@@ -542,6 +565,20 @@ var Q402NodeClient = class _Q402NodeClient {
|
|
|
542
565
|
return data;
|
|
543
566
|
}
|
|
544
567
|
};
|
|
568
|
+
var BatchPayError = class extends Error {
|
|
569
|
+
aborted;
|
|
570
|
+
totalSuccess;
|
|
571
|
+
totalFailed;
|
|
572
|
+
results;
|
|
573
|
+
constructor(message, details) {
|
|
574
|
+
super(message);
|
|
575
|
+
this.name = "BatchPayError";
|
|
576
|
+
this.aborted = details.aborted;
|
|
577
|
+
this.totalSuccess = details.totalSuccess;
|
|
578
|
+
this.totalFailed = details.totalFailed;
|
|
579
|
+
this.results = details.results;
|
|
580
|
+
}
|
|
581
|
+
};
|
|
545
582
|
function sandboxPay(chain, input) {
|
|
546
583
|
const tokenCfg = tokenFor(chain, input.token);
|
|
547
584
|
const tokenAmount = toRawAmount(input.amount, tokenCfg.decimals);
|
|
@@ -678,7 +715,7 @@ var RECIPIENT_LIMIT_TRIAL = 5;
|
|
|
678
715
|
var RECIPIENT_LIMIT_PAID = 20;
|
|
679
716
|
var CLIENT_RECIPIENT_CAP = RECIPIENT_LIMIT_PAID;
|
|
680
717
|
var BatchPayInputSchema = z3.object({
|
|
681
|
-
chain: z3.enum(["avax", "bnb", "eth", "
|
|
718
|
+
chain: z3.enum(["avax", "bnb", "eth", "mantle", "injective"]),
|
|
682
719
|
token: z3.enum(["USDC", "USDT", "RLUSD"]).describe(
|
|
683
720
|
"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
721
|
),
|
|
@@ -741,6 +778,7 @@ async function runBatchPay(input) {
|
|
|
741
778
|
guardsApplied.push("mode=sandbox");
|
|
742
779
|
return {
|
|
743
780
|
mode: "sandbox",
|
|
781
|
+
status: "sandbox",
|
|
744
782
|
result: { sandbox: sandboxResults, reason: describeSandboxReason2() },
|
|
745
783
|
guardsApplied,
|
|
746
784
|
setupHint: describeSandboxReason2()
|
|
@@ -752,13 +790,38 @@ async function runBatchPay(input) {
|
|
|
752
790
|
chain,
|
|
753
791
|
relayBaseUrl: CONFIG.relayBaseUrl
|
|
754
792
|
});
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
793
|
+
try {
|
|
794
|
+
const result = await client.batchPay({
|
|
795
|
+
token: input.token,
|
|
796
|
+
recipients: input.recipients.map((r) => ({ to: r.to, amount: r.amount }))
|
|
797
|
+
});
|
|
798
|
+
guardsApplied.push("mode=live");
|
|
799
|
+
guardsApplied.push(`scope=${result.scope} (server enforced)`);
|
|
800
|
+
guardsApplied.push(`batch_size=${input.recipients.length}/${result.limit}`);
|
|
801
|
+
return { mode: "live", status: "success", result, guardsApplied };
|
|
802
|
+
} catch (err) {
|
|
803
|
+
if (err instanceof BatchPayError) {
|
|
804
|
+
guardsApplied.push("mode=live");
|
|
805
|
+
guardsApplied.push(`batch_${err.aborted ? "aborted" : "partial_failure"}`);
|
|
806
|
+
const status = err.aborted ? "aborted" : "partial_failure";
|
|
807
|
+
return {
|
|
808
|
+
mode: "live",
|
|
809
|
+
status,
|
|
810
|
+
result: {
|
|
811
|
+
ok: false,
|
|
812
|
+
scope: "paid",
|
|
813
|
+
limit: input.recipients.length,
|
|
814
|
+
totalSuccess: err.totalSuccess,
|
|
815
|
+
totalFailed: err.totalFailed,
|
|
816
|
+
aborted: err.aborted,
|
|
817
|
+
results: err.results
|
|
818
|
+
},
|
|
819
|
+
guardsApplied,
|
|
820
|
+
error: err.message
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
throw err;
|
|
824
|
+
}
|
|
762
825
|
}
|
|
763
826
|
function describeSandboxReason2() {
|
|
764
827
|
const missing = [];
|
|
@@ -770,14 +833,17 @@ function describeSandboxReason2() {
|
|
|
770
833
|
}
|
|
771
834
|
var BATCH_PAY_TOOL = {
|
|
772
835
|
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
|
|
836
|
+
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 across 5 EIP-7702 default chains (avax, bnb, eth, mantle, injective). xlayer + stable are NOT batchable \u2014 use q402_pay in a loop. 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
837
|
inputSchema: {
|
|
775
838
|
type: "object",
|
|
776
839
|
properties: {
|
|
777
840
|
chain: {
|
|
778
841
|
type: "string",
|
|
779
|
-
|
|
780
|
-
|
|
842
|
+
// Narrower than the full chain set — xlayer and stable are NOT batchable
|
|
843
|
+
// (chain-specific nonce field shapes). Use q402_pay in a loop for
|
|
844
|
+
// those chains.
|
|
845
|
+
enum: ["avax", "bnb", "eth", "mantle", "injective"],
|
|
846
|
+
description: "Target chain. Applies to every recipient in the batch. xlayer + stable are NOT supported here \u2014 use q402_pay in a loop."
|
|
781
847
|
},
|
|
782
848
|
token: {
|
|
783
849
|
type: "string",
|
|
@@ -1028,7 +1094,7 @@ var RECEIPT_TOOL = {
|
|
|
1028
1094
|
|
|
1029
1095
|
// src/index.ts
|
|
1030
1096
|
var PACKAGE_NAME = "@quackai/q402-mcp";
|
|
1031
|
-
var PACKAGE_VERSION = "0.3.
|
|
1097
|
+
var PACKAGE_VERSION = "0.3.11";
|
|
1032
1098
|
function jsonText(value) {
|
|
1033
1099
|
return { type: "text", text: JSON.stringify(value, null, 2) };
|
|
1034
1100
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quackai/q402-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
4
4
|
"description": "MCP server for Q402 — gasless USDC, USDT, and RLUSD payments across 7 EVM chains, 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": [
|