@subly_fi/pay 0.7.0 → 0.7.2

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
@@ -9,12 +9,12 @@ Version 0.7 is beta software and has not had an external security audit. Vault o
9
9
  You need **Node.js 24+**, a Solana agent wallet with mainnet USDC, a trusted relayer URL, and a mainnet RPC endpoint that supports transaction simulation with inner instructions. Your wallet can be a local keypair or a supported custody signer. The owner who approves spending controls can use a passkey or a separate Solana wallet.
10
10
 
11
11
  ```bash
12
- npx -y @subly_fi/pay@0.7.0 --help
12
+ npx -y @subly_fi/pay@0.7.2 --help
13
13
  export SUBLY_RELAYER_URL=https://your-relayer.example.com
14
14
  export SOLANA_RPC_URL=https://your-mainnet-rpc.example.com
15
15
  export SUBLY_DEMO_AGENT_KEYPAIR_PATH=/absolute/path/to/agent.json
16
- npx -y @subly_fi/pay@0.7.0 doctor
17
- npx -y @subly_fi/pay@0.7.0 vaults
16
+ npx -y @subly_fi/pay@0.7.2 doctor
17
+ npx -y @subly_fi/pay@0.7.2 vaults
18
18
  ```
19
19
 
20
20
  Use an existing dedicated agent wallet, or create one with `solana-keygen new -o agent.json`. Keep its recovery material private and restrict the file to its owner (`chmod 600 agent.json`). Fund its public address with **USDC on Solana mainnet**. Subly does not create or fund wallets. Vault fees require a funded relayer sponsor; the final API payment requires the seller's facilitator fee payer.
@@ -23,28 +23,28 @@ Use an existing dedicated agent wallet, or create one with `solana-keygen new -o
23
23
  2. Create an owner setup link. This example pre-approves a **1.01 USDC** deposit; the selected vault's minimum can differ:
24
24
 
25
25
  ```bash
26
- npx -y @subly_fi/pay@0.7.0 setup-link --initial-deposit 1010000
26
+ npx -y @subly_fi/pay@0.7.2 setup-link --initial-deposit 1010000
27
27
  ```
28
28
 
29
29
  Open the returned `setupUrl`, review the wallet, vault and limits, then approve with your passkey or wallet. Links expire in 10 minutes. Treat setup and approval links as private capabilities. The first person completing an initial setup becomes the owner for that wallet/vault.
30
30
  3. Check completion and deposit promptly; initial deposit approval lasts about 15 minutes:
31
31
 
32
32
  ```bash
33
- npx -y @subly_fi/pay@0.7.0 setup-status <sessionId>
34
- npx -y @subly_fi/pay@0.7.0 deposit 1010000
35
- npx -y @subly_fi/pay@0.7.0 budget
33
+ npx -y @subly_fi/pay@0.7.2 setup-status <sessionId>
34
+ npx -y @subly_fi/pay@0.7.2 deposit 1010000
35
+ npx -y @subly_fi/pay@0.7.2 budget
36
36
  ```
37
37
 
38
38
  4. Wait until **spendable yield** covers the price and vault fees. A new deposit does not immediately provide a payment budget. Then request a compatible API:
39
39
 
40
40
  ```bash
41
- npx -y @subly_fi/pay@0.7.0 fetch https://seller.example.com/paid-resource
41
+ npx -y @subly_fi/pay@0.7.2 fetch https://seller.example.com/paid-resource
42
42
  ```
43
43
 
44
44
  5. Withdraw funds back to the same agent wallet when needed:
45
45
 
46
46
  ```bash
47
- npx -y @subly_fi/pay@0.7.0 withdraw 1000000
47
+ npx -y @subly_fi/pay@0.7.2 withdraw 1000000
48
48
  ```
49
49
 
50
50
  All amounts are raw USDC integers: `1000000` = 1 USDC. `fetch` defaults to a **0.01 USDC cap**, configurable with `SUBLY_MCP_MAX_AMOUNT_RAW_USDC` or `fetch <URL> <capRawUSDC>`. The owner mandate may set stricter limits. `setup-link --help` lists policy options. A withdrawal can include principal and is subject to liquidity, fees and the owner's policy. A revoked mandate also blocks relayer withdrawals.
@@ -60,7 +60,7 @@ Add this to the MCP configuration of your editor or agent host. Replace all exam
60
60
  "mcpServers": {
61
61
  "subly": {
62
62
  "command": "npx",
63
- "args": ["-y", "@subly_fi/pay@0.7.0", "mcp"],
63
+ "args": ["-y", "@subly_fi/pay@0.7.2", "mcp"],
64
64
  "env": {
65
65
  "SUBLY_RELAYER_URL": "https://your-relayer.example.com",
66
66
  "SOLANA_RPC_URL": "https://your-mainnet-rpc.example.com",
package/dist/budget.js CHANGED
@@ -1747,8 +1747,11 @@ async function agentWalletSignerFromEnv(env = process.env) {
1747
1747
  );
1748
1748
  }
1749
1749
 
1750
+ // ../../src/domain/withdrawal-rounding.ts
1751
+ var WITHDRAWAL_ROUNDING_RAW_USDC = 10n;
1752
+ var YIELD_REALIZE_ROUNDING_RAW_USDC = WITHDRAWAL_ROUNDING_RAW_USDC / 2n;
1753
+
1750
1754
  // ../../src/client/withdrawal-preview.ts
1751
- var ROUNDING_RAW_USDC = 10n;
1752
1755
  async function assertWithdrawalPreview(input) {
1753
1756
  const destination = deriveAssociatedTokenAddress({ owner: input.wallet, mint: input.vault.usdcMint });
1754
1757
  const simulation = await input.rpc.simulateTransaction(
@@ -1781,7 +1784,8 @@ async function assertWithdrawalPreview(input) {
1781
1784
  if (info.source === destination) received -= amount;
1782
1785
  }
1783
1786
  }
1784
- if (received <= 0n || received > input.amountRawUsdc + ROUNDING_RAW_USDC || received < input.amountRawUsdc - ROUNDING_RAW_USDC) {
1787
+ const minimum = input.purpose === "yield_realize" ? input.amountRawUsdc : input.amountRawUsdc - WITHDRAWAL_ROUNDING_RAW_USDC;
1788
+ if (received <= 0n || received > input.amountRawUsdc + WITHDRAWAL_ROUNDING_RAW_USDC || received < minimum) {
1785
1789
  throw new Error("Withdrawal preview differs from the requested USDC amount; no transaction was signed");
1786
1790
  }
1787
1791
  }
@@ -1988,7 +1992,8 @@ var VaultFlowClient = class {
1988
1992
  serializedTransaction: prepared.serializedTransaction,
1989
1993
  wallet: this.signer.walletAddress,
1990
1994
  vault: this.vault,
1991
- amountRawUsdc: input.amountRawUsdc
1995
+ amountRawUsdc: input.amountRawUsdc,
1996
+ ...input.purpose === void 0 ? {} : { purpose: input.purpose }
1992
1997
  });
1993
1998
  const signed = await this.signer.signWithdrawal({
1994
1999
  intent: prepared.signingIntent,
package/dist/deposit.js CHANGED
@@ -1843,8 +1843,11 @@ async function agentWalletSignerFromEnv(env = process.env) {
1843
1843
  );
1844
1844
  }
1845
1845
 
1846
+ // ../../src/domain/withdrawal-rounding.ts
1847
+ var WITHDRAWAL_ROUNDING_RAW_USDC = 10n;
1848
+ var YIELD_REALIZE_ROUNDING_RAW_USDC = WITHDRAWAL_ROUNDING_RAW_USDC / 2n;
1849
+
1846
1850
  // ../../src/client/withdrawal-preview.ts
1847
- var ROUNDING_RAW_USDC = 10n;
1848
1851
  async function assertWithdrawalPreview(input) {
1849
1852
  const destination = deriveAssociatedTokenAddress({ owner: input.wallet, mint: input.vault.usdcMint });
1850
1853
  const simulation = await input.rpc.simulateTransaction(
@@ -1877,7 +1880,8 @@ async function assertWithdrawalPreview(input) {
1877
1880
  if (info.source === destination) received -= amount;
1878
1881
  }
1879
1882
  }
1880
- if (received <= 0n || received > input.amountRawUsdc + ROUNDING_RAW_USDC || received < input.amountRawUsdc - ROUNDING_RAW_USDC) {
1883
+ const minimum = input.purpose === "yield_realize" ? input.amountRawUsdc : input.amountRawUsdc - WITHDRAWAL_ROUNDING_RAW_USDC;
1884
+ if (received <= 0n || received > input.amountRawUsdc + WITHDRAWAL_ROUNDING_RAW_USDC || received < minimum) {
1881
1885
  throw new Error("Withdrawal preview differs from the requested USDC amount; no transaction was signed");
1882
1886
  }
1883
1887
  }
@@ -2047,7 +2051,8 @@ var VaultFlowClient = class {
2047
2051
  serializedTransaction: prepared.serializedTransaction,
2048
2052
  wallet: this.signer.walletAddress,
2049
2053
  vault: this.vault,
2050
- amountRawUsdc: input.amountRawUsdc
2054
+ amountRawUsdc: input.amountRawUsdc,
2055
+ ...input.purpose === void 0 ? {} : { purpose: input.purpose }
2051
2056
  });
2052
2057
  const signed = await this.signer.signWithdrawal({
2053
2058
  intent: prepared.signingIntent,
package/dist/doctor.js CHANGED
@@ -169,18 +169,18 @@ await check("relayer and vault", async () => {
169
169
  if (url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))) throw new Error("Use HTTPS (HTTP is allowed only for local development)");
170
170
  const catalog = vaultCatalogFromEnv();
171
171
  const local = defaultCatalogVault(catalog);
172
- const health = await fetch(`${url.toString().replace(/\/$/, "")}/healthz`, { signal: AbortSignal.timeout(1e4) });
173
- if (!health.ok || (await health.json()).ok !== true) throw new Error("Relayer health check failed");
172
+ const health = await fetch(`${url.toString().replace(/\/$/, "")}/readyz`, { signal: AbortSignal.timeout(1e4) });
173
+ if (!health.ok || (await health.json()).ok !== true) throw new Error("Relayer readiness check failed");
174
174
  const response = await fetch(`${url.toString().replace(/\/$/, "")}/v1/vaults`, { signal: AbortSignal.timeout(1e4) });
175
175
  if (!response.ok) throw new Error("Relayer vault catalogue unavailable");
176
176
  const remote = (await response.json()).vaults?.find((v) => v.address === local.address);
177
177
  if (!remote || ["programId", "usdcMint", "shareMint", "farm"].some((key) => remote[key] !== local[key])) throw new Error("Selected local vault trust anchors differ from the relayer; review the operator catalogue before changing configuration");
178
- return `Healthy; selected vault ${local.address} matches local trust anchors`;
178
+ return `Ready; selected vault ${local.address} matches local trust anchors`;
179
179
  });
180
180
  await check("Solana RPC", async () => {
181
181
  const response = await fetch(env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "getGenesisHash" }), signal: AbortSignal.timeout(1e4) });
182
182
  const body = await response.json();
183
- if (!response.ok || body.result !== "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp") throw new Error("RPC is unavailable or is not Solana mainnet-beta");
183
+ if (!response.ok || body.result !== "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d") throw new Error("RPC is unavailable or is not Solana mainnet-beta");
184
184
  return "Mainnet-beta RPC reachable";
185
185
  });
186
186
  var ok = checks.every((c) => c.ok);
@@ -274,8 +274,11 @@ function modPow(base, exponent, modulus) {
274
274
  return result;
275
275
  }
276
276
 
277
+ // ../../src/domain/withdrawal-rounding.ts
278
+ var WITHDRAWAL_ROUNDING_RAW_USDC = 10n;
279
+ var YIELD_REALIZE_ROUNDING_RAW_USDC = WITHDRAWAL_ROUNDING_RAW_USDC / 2n;
280
+
277
281
  // ../../src/client/withdrawal-preview.ts
278
- var ROUNDING_RAW_USDC = 10n;
279
282
  async function assertWithdrawalPreview(input) {
280
283
  const destination = deriveAssociatedTokenAddress({ owner: input.wallet, mint: input.vault.usdcMint });
281
284
  const simulation = await input.rpc.simulateTransaction(
@@ -308,7 +311,8 @@ async function assertWithdrawalPreview(input) {
308
311
  if (info.source === destination) received -= amount;
309
312
  }
310
313
  }
311
- if (received <= 0n || received > input.amountRawUsdc + ROUNDING_RAW_USDC || received < input.amountRawUsdc - ROUNDING_RAW_USDC) {
314
+ const minimum = input.purpose === "yield_realize" ? input.amountRawUsdc : input.amountRawUsdc - WITHDRAWAL_ROUNDING_RAW_USDC;
315
+ if (received <= 0n || received > input.amountRawUsdc + WITHDRAWAL_ROUNDING_RAW_USDC || received < minimum) {
312
316
  throw new Error("Withdrawal preview differs from the requested USDC amount; no transaction was signed");
313
317
  }
314
318
  }
@@ -515,7 +519,8 @@ var VaultFlowClient = class {
515
519
  serializedTransaction: prepared.serializedTransaction,
516
520
  wallet: this.signer.walletAddress,
517
521
  vault: this.vault,
518
- amountRawUsdc: input.amountRawUsdc
522
+ amountRawUsdc: input.amountRawUsdc,
523
+ ...input.purpose === void 0 ? {} : { purpose: input.purpose }
519
524
  });
520
525
  const signed = await this.signer.signWithdrawal({
521
526
  intent: prepared.signingIntent,
@@ -1013,6 +1018,7 @@ function hashStableJson(value) {
1013
1018
 
1014
1019
  // ../../src/x402/headers.ts
1015
1020
  var PAYMENT_REQUIRED_HEADER = "payment-required";
1021
+ var PAYMENT_RESPONSE_HEADER = "payment-response";
1016
1022
  var MAX_HEADER_JSON_BYTES = 16384;
1017
1023
  var X402HeaderError = class extends Error {
1018
1024
  reason;
@@ -1367,6 +1373,13 @@ var StandardX402Payer = class {
1367
1373
  { error, pendingPayment: pendingRecord }
1368
1374
  );
1369
1375
  }
1376
+ if (realized.realizedRawUsdc < selected2.amountRawUsdc) {
1377
+ throw new StandardX402PayError(
1378
+ "realize_underfunded",
1379
+ "The confirmed withdrawal did not cover the exact API price. No external payment was attempted; reconcile the recorded withdrawal before retrying.",
1380
+ { pendingPayment: pendingRecord }
1381
+ );
1382
+ }
1370
1383
  let response;
1371
1384
  try {
1372
1385
  response = await this.x402Fetch(input.url, init, selected2);
@@ -1532,7 +1545,7 @@ function pendingPaymentKey(input) {
1532
1545
  return `${input.method}:${input.url}:${input.requestBodyHash}`;
1533
1546
  }
1534
1547
  function extractSettledPaymentTxSignature(response) {
1535
- const header = response.headers.get("x-payment-response");
1548
+ const header = response.headers.get(PAYMENT_RESPONSE_HEADER) ?? response.headers.get("x-payment-response");
1536
1549
  if (header === null || header.length === 0) {
1537
1550
  return null;
1538
1551
  }
package/dist/pay.js CHANGED
@@ -328,8 +328,11 @@ function modPow(base, exponent, modulus) {
328
328
  return result;
329
329
  }
330
330
 
331
+ // ../../src/domain/withdrawal-rounding.ts
332
+ var WITHDRAWAL_ROUNDING_RAW_USDC = 10n;
333
+ var YIELD_REALIZE_ROUNDING_RAW_USDC = WITHDRAWAL_ROUNDING_RAW_USDC / 2n;
334
+
331
335
  // ../../src/client/withdrawal-preview.ts
332
- var ROUNDING_RAW_USDC = 10n;
333
336
  async function assertWithdrawalPreview(input) {
334
337
  const destination = deriveAssociatedTokenAddress({ owner: input.wallet, mint: input.vault.usdcMint });
335
338
  const simulation = await input.rpc.simulateTransaction(
@@ -362,7 +365,8 @@ async function assertWithdrawalPreview(input) {
362
365
  if (info.source === destination) received -= amount;
363
366
  }
364
367
  }
365
- if (received <= 0n || received > input.amountRawUsdc + ROUNDING_RAW_USDC || received < input.amountRawUsdc - ROUNDING_RAW_USDC) {
368
+ const minimum = input.purpose === "yield_realize" ? input.amountRawUsdc : input.amountRawUsdc - WITHDRAWAL_ROUNDING_RAW_USDC;
369
+ if (received <= 0n || received > input.amountRawUsdc + WITHDRAWAL_ROUNDING_RAW_USDC || received < minimum) {
366
370
  throw new Error("Withdrawal preview differs from the requested USDC amount; no transaction was signed");
367
371
  }
368
372
  }
@@ -532,7 +536,8 @@ var VaultFlowClient = class {
532
536
  serializedTransaction: prepared.serializedTransaction,
533
537
  wallet: this.signer.walletAddress,
534
538
  vault: this.vault,
535
- amountRawUsdc: input.amountRawUsdc
539
+ amountRawUsdc: input.amountRawUsdc,
540
+ ...input.purpose === void 0 ? {} : { purpose: input.purpose }
536
541
  });
537
542
  const signed = await this.signer.signWithdrawal({
538
543
  intent: prepared.signingIntent,
@@ -965,6 +970,7 @@ function sha256HexOf(data) {
965
970
  // ../../src/x402/headers.ts
966
971
  import { z as z2 } from "zod";
967
972
  var PAYMENT_REQUIRED_HEADER = "payment-required";
973
+ var PAYMENT_RESPONSE_HEADER = "payment-response";
968
974
  var MAX_HEADER_JSON_BYTES = 16384;
969
975
  var X402HeaderError = class extends Error {
970
976
  reason;
@@ -1303,6 +1309,13 @@ var StandardX402Payer = class {
1303
1309
  { error, pendingPayment: pendingRecord }
1304
1310
  );
1305
1311
  }
1312
+ if (realized.realizedRawUsdc < selected.amountRawUsdc) {
1313
+ throw new StandardX402PayError(
1314
+ "realize_underfunded",
1315
+ "The confirmed withdrawal did not cover the exact API price. No external payment was attempted; reconcile the recorded withdrawal before retrying.",
1316
+ { pendingPayment: pendingRecord }
1317
+ );
1318
+ }
1306
1319
  let response;
1307
1320
  try {
1308
1321
  response = await this.x402Fetch(input.url, init, selected);
@@ -1468,7 +1481,7 @@ function pendingPaymentKey(input) {
1468
1481
  return `${input.method}:${input.url}:${input.requestBodyHash}`;
1469
1482
  }
1470
1483
  function extractSettledPaymentTxSignature(response) {
1471
- const header = response.headers.get("x-payment-response");
1484
+ const header = response.headers.get(PAYMENT_RESPONSE_HEADER) ?? response.headers.get("x-payment-response");
1472
1485
  if (header === null || header.length === 0) {
1473
1486
  return null;
1474
1487
  }
@@ -1747,8 +1747,11 @@ async function agentWalletSignerFromEnv(env = process.env) {
1747
1747
  );
1748
1748
  }
1749
1749
 
1750
+ // ../../src/domain/withdrawal-rounding.ts
1751
+ var WITHDRAWAL_ROUNDING_RAW_USDC = 10n;
1752
+ var YIELD_REALIZE_ROUNDING_RAW_USDC = WITHDRAWAL_ROUNDING_RAW_USDC / 2n;
1753
+
1750
1754
  // ../../src/client/withdrawal-preview.ts
1751
- var ROUNDING_RAW_USDC = 10n;
1752
1755
  async function assertWithdrawalPreview(input) {
1753
1756
  const destination = deriveAssociatedTokenAddress({ owner: input.wallet, mint: input.vault.usdcMint });
1754
1757
  const simulation = await input.rpc.simulateTransaction(
@@ -1781,7 +1784,8 @@ async function assertWithdrawalPreview(input) {
1781
1784
  if (info.source === destination) received -= amount;
1782
1785
  }
1783
1786
  }
1784
- if (received <= 0n || received > input.amountRawUsdc + ROUNDING_RAW_USDC || received < input.amountRawUsdc - ROUNDING_RAW_USDC) {
1787
+ const minimum = input.purpose === "yield_realize" ? input.amountRawUsdc : input.amountRawUsdc - WITHDRAWAL_ROUNDING_RAW_USDC;
1788
+ if (received <= 0n || received > input.amountRawUsdc + WITHDRAWAL_ROUNDING_RAW_USDC || received < minimum) {
1785
1789
  throw new Error("Withdrawal preview differs from the requested USDC amount; no transaction was signed");
1786
1790
  }
1787
1791
  }
@@ -1988,7 +1992,8 @@ var VaultFlowClient = class {
1988
1992
  serializedTransaction: prepared.serializedTransaction,
1989
1993
  wallet: this.signer.walletAddress,
1990
1994
  vault: this.vault,
1991
- amountRawUsdc: input.amountRawUsdc
1995
+ amountRawUsdc: input.amountRawUsdc,
1996
+ ...input.purpose === void 0 ? {} : { purpose: input.purpose }
1992
1997
  });
1993
1998
  const signed = await this.signer.signWithdrawal({
1994
1999
  intent: prepared.signingIntent,
package/dist/withdraw.js CHANGED
@@ -1747,8 +1747,11 @@ async function agentWalletSignerFromEnv(env = process.env) {
1747
1747
  );
1748
1748
  }
1749
1749
 
1750
+ // ../../src/domain/withdrawal-rounding.ts
1751
+ var WITHDRAWAL_ROUNDING_RAW_USDC = 10n;
1752
+ var YIELD_REALIZE_ROUNDING_RAW_USDC = WITHDRAWAL_ROUNDING_RAW_USDC / 2n;
1753
+
1750
1754
  // ../../src/client/withdrawal-preview.ts
1751
- var ROUNDING_RAW_USDC = 10n;
1752
1755
  async function assertWithdrawalPreview(input) {
1753
1756
  const destination = deriveAssociatedTokenAddress({ owner: input.wallet, mint: input.vault.usdcMint });
1754
1757
  const simulation = await input.rpc.simulateTransaction(
@@ -1781,7 +1784,8 @@ async function assertWithdrawalPreview(input) {
1781
1784
  if (info.source === destination) received -= amount;
1782
1785
  }
1783
1786
  }
1784
- if (received <= 0n || received > input.amountRawUsdc + ROUNDING_RAW_USDC || received < input.amountRawUsdc - ROUNDING_RAW_USDC) {
1787
+ const minimum = input.purpose === "yield_realize" ? input.amountRawUsdc : input.amountRawUsdc - WITHDRAWAL_ROUNDING_RAW_USDC;
1788
+ if (received <= 0n || received > input.amountRawUsdc + WITHDRAWAL_ROUNDING_RAW_USDC || received < minimum) {
1785
1789
  throw new Error("Withdrawal preview differs from the requested USDC amount; no transaction was signed");
1786
1790
  }
1787
1791
  }
@@ -1988,7 +1992,8 @@ var VaultFlowClient = class {
1988
1992
  serializedTransaction: prepared.serializedTransaction,
1989
1993
  wallet: this.signer.walletAddress,
1990
1994
  vault: this.vault,
1991
- amountRawUsdc: input.amountRawUsdc
1995
+ amountRawUsdc: input.amountRawUsdc,
1996
+ ...input.purpose === void 0 ? {} : { purpose: input.purpose }
1992
1997
  });
1993
1998
  const signed = await this.signer.signWithdrawal({
1994
1999
  intent: prepared.signingIntent,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@subly_fi/pay",
3
- "version": "0.7.0",
4
- "description": "Subly client: pay compatible standard x402 Solana USDC exact APIs from Kamino vault yield \u2014 the seller needs no Subly integration. Ships an MCP server and a one-shot pay/deposit CLI; non-custodial (signs locally with your own key).",
3
+ "version": "0.7.2",
4
+ "description": "Subly client: pay compatible standard x402 Solana USDC exact APIs from Kamino vault yield the seller needs no Subly integration. Ships an MCP server and a one-shot pay/deposit CLI; non-custodial (signs locally with your own key).",
5
5
  "keywords": [
6
6
  "x402",
7
7
  "solana",