@elisym/cli 0.26.1 → 0.27.0

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/dist/index.js CHANGED
@@ -2,8 +2,8 @@
2
2
  import { ReadableStream } from 'node:stream/web';
3
3
  import { readFileSync, existsSync, readdirSync, statSync, renameSync, chmodSync, mkdirSync, writeFileSync, appendFileSync, unlinkSync, rmdirSync, realpathSync } from 'node:fs';
4
4
  import { dirname, join, resolve, basename, extname, relative, sep } from 'node:path';
5
- import { SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, formatSol, formatAssetAmount, USDC_SOLANA_DEVNET, signerFromSecretKeyBase58, ElisymClient, KIND_EXTERNAL_IDENTITIES, POLICY_D_TAG_PREFIX, KIND_LONG_FORM_ARTICLE, jobRequestKind, DEFAULT_KIND_OFFSET, toDTag, DEFAULTS, createBlossomTransport, generateSolanaWallet, verifyAgentIdentities, getProtocolConfig, getProtocolProgramId, calculateProtocolFee, LIMITS, makeCensor, DEFAULT_REDACT_PATHS, POLICY_T_TAG, SESSION_ID_REGEX, createSlidingWindowLimiter, utf8ByteLength, readAcceptedTransports, resolveDelegationAsset, MAX_PROOF_TTL_SECS, PROOF_CLOCK_SKEW_SECS, verifyDelegationAuthProof, deriveOwnerDelegationAta, getDelegation, buildDelegatedTransfer, buildSignedPull, sendConfirmToTerminal, parseDelegatedPayment, confirmPullToTerminal, isDefinitelyUnpaid, decodeJobPayload, BoundedSet, KIND_JOB_FEEDBACK, encodeSecretKeyBase58, GITHUB_USERNAME_REGEX, X_USERNAME_REGEX, normalizeNip05Identifier, splitNip05Identifier, NATIVE_SOL, GIST_ID_REGEX, TWEET_ID_REGEX } from '@elisym/sdk';
6
- import { ElisymYamlSchema, resolveInHome, resolveInProject, createAgentDir, writeYamlInitial, writeExampleSkillTemplate, writeSecrets, listAgents, loadAgent, writeYaml, agentPaths, readMediaCache, loadPoliciesFromDir, ensureGitignoreHasIrohEntry, ensureGitignoreHasX402Entries, ensureGitignoreHasSessionsEntry, ensureGitignoreHasDelegationNoncesEntry, lookupCachedUrl, newCacheEntry, writeMediaCache } from '@elisym/sdk/agent-store';
5
+ import { SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, splAssetsForNetwork, formatSol, formatAssetAmount, signerFromSecretKeyBase58, resolveUsdcAsset, ElisymClient, KIND_EXTERNAL_IDENTITIES, POLICY_D_TAG_PREFIX, KIND_LONG_FORM_ARTICLE, jobRequestKind, DEFAULT_KIND_OFFSET, toDTag, DEFAULTS, createBlossomTransport, generateSolanaWallet, verifyAgentIdentities, getProtocolConfig, getProtocolProgramId, calculateProtocolFee, LIMITS, makeCensor, DEFAULT_REDACT_PATHS, POLICY_T_TAG, SESSION_ID_REGEX, createSlidingWindowLimiter, utf8ByteLength, readAcceptedTransports, resolveDelegationAsset, MAX_PROOF_TTL_SECS, PROOF_CLOCK_SKEW_SECS, verifyDelegationAuthProof, deriveOwnerDelegationAta, getDelegation, buildDelegatedTransfer, buildSignedPull, sendConfirmToTerminal, parseDelegatedPayment, confirmPullToTerminal, isDefinitelyUnpaid, decodeJobPayload, BoundedSet, KIND_JOB_FEEDBACK, encodeSecretKeyBase58, GITHUB_USERNAME_REGEX, X_USERNAME_REGEX, normalizeNip05Identifier, splitNip05Identifier, NATIVE_SOL, GIST_ID_REGEX, TWEET_ID_REGEX } from '@elisym/sdk';
6
+ import { ElisymYamlSchema, resolveInHome, resolveInProject, createAgentDir, writeYamlInitial, writeExampleSkillTemplate, writeSecrets, listAgents, loadAgent, writeYaml, agentPaths, readMediaCache, loadPoliciesFromDir, ensureGitignoreHasIrohEntry, ensureGitignoreHasX402Entries, ensureGitignoreHasSessionsEntry, ensureGitignoreHasDelegationNoncesEntry, readAgentPublic, lookupCachedUrl, newCacheEntry, writeMediaCache } from '@elisym/sdk/agent-store';
7
7
  import { isAddress, createSolanaRpc, address, signature } from '@solana/kit';
8
8
  import { generateSecretKey, getPublicKey, nip19, verifyEvent } from 'nostr-tools';
9
9
  import YAML2 from 'yaml';
@@ -1064,8 +1064,18 @@ var init_llm = __esm({
1064
1064
  var init_exports = {};
1065
1065
  __export(init_exports, {
1066
1066
  cmdInit: () => cmdInit,
1067
- fetchModels: () => fetchModels3
1067
+ fetchModels: () => fetchModels3,
1068
+ parseNetworkOption: () => parseNetworkOption
1068
1069
  });
1070
+ function parseNetworkOption(raw) {
1071
+ if (raw === void 0) {
1072
+ return void 0;
1073
+ }
1074
+ if (raw === "devnet" || raw === "mainnet") {
1075
+ return raw;
1076
+ }
1077
+ throw new Error(`--network must be "devnet" or "mainnet"; got "${raw}"`);
1078
+ }
1069
1079
  function buildDefaultYaml() {
1070
1080
  return ElisymYamlSchema.parse({
1071
1081
  description: "An elisym AI agent",
@@ -1097,6 +1107,7 @@ async function cmdInit(nameArg, options = {}) {
1097
1107
  if (options.config && options.defaults) {
1098
1108
  throw new Error("--config and --defaults are mutually exclusive; pick one.");
1099
1109
  }
1110
+ const networkFlag = parseNetworkOption(options.network);
1100
1111
  const skipConfirms = options.yes || options.defaults;
1101
1112
  let template;
1102
1113
  if (options.config) {
@@ -1105,9 +1116,20 @@ async function cmdInit(nameArg, options = {}) {
1105
1116
  template = ElisymYamlSchema.parse(YAML2.parse(raw) ?? {});
1106
1117
  console.log(` Loaded template from ${configPath}
1107
1118
  `);
1119
+ const templateSolana = template.payments.find((entry) => entry.chain === "solana");
1120
+ if (networkFlag !== void 0 && templateSolana && templateSolana.network !== networkFlag) {
1121
+ throw new Error(
1122
+ `--network ${networkFlag} conflicts with the template's payments[].network (${templateSolana.network}). Edit the template or drop the flag.`
1123
+ );
1124
+ }
1108
1125
  } else if (options.defaults) {
1109
1126
  template = buildDefaultYaml();
1110
1127
  console.log(" Using default skeleton (no LLM, no payments). Edit later via `profile`.\n");
1128
+ if (networkFlag !== void 0) {
1129
+ console.warn(
1130
+ " ! --network has no effect with --defaults (no payments entry is created). The wallet network is chosen when you configure a wallet.\n"
1131
+ );
1132
+ }
1111
1133
  }
1112
1134
  const agentName = await resolveAgentName2(nameArg, inquirer);
1113
1135
  const target = pickTarget(options);
@@ -1166,7 +1188,7 @@ async function cmdInit(nameArg, options = {}) {
1166
1188
  if (template) {
1167
1189
  yaml = template;
1168
1190
  } else {
1169
- const result = await promptYaml(inquirer);
1191
+ const result = await promptYaml(inquirer, networkFlag);
1170
1192
  yaml = result.yaml;
1171
1193
  promptedApiKey = result.apiKey;
1172
1194
  }
@@ -1259,6 +1281,14 @@ async function cmdInit(nameArg, options = {}) {
1259
1281
  ]);
1260
1282
  }
1261
1283
  }
1284
+ const mainnetBound = yaml.payments.some(
1285
+ (entry) => entry.chain === "solana" && entry.network === "mainnet"
1286
+ );
1287
+ if (mainnetBound && !passphrase) {
1288
+ console.warn(
1289
+ " ! No passphrase set: this MAINNET agent will store its secrets unencrypted on disk.\n Strongly consider one (--passphrase or ELISYM_PASSPHRASE) - its keys will guard real funds."
1290
+ );
1291
+ }
1262
1292
  const nostrSecretBytes = generateSecretKey();
1263
1293
  const nostrPubkey = getPublicKey(nostrSecretBytes);
1264
1294
  const nostrSecretHex = Buffer.from(nostrSecretBytes).toString("hex");
@@ -1284,7 +1314,7 @@ async function cmdInit(nameArg, options = {}) {
1284
1314
  console.log(` Location: ${created.dir}`);
1285
1315
  console.log(` Nostr: ${npub}`);
1286
1316
  if (yaml.payments[0]?.address) {
1287
- console.log(` Solana: ${yaml.payments[0].address}`);
1317
+ console.log(` Solana: ${yaml.payments[0].address} (${yaml.payments[0].network})`);
1288
1318
  }
1289
1319
  if (passphrase) {
1290
1320
  console.log(" Secrets encrypted with your passphrase.");
@@ -1314,7 +1344,7 @@ async function resolveAgentName2(nameArg, inquirer) {
1314
1344
  validateAgentName(inputName);
1315
1345
  return inputName;
1316
1346
  }
1317
- async function promptYaml(inquirer) {
1347
+ async function promptYaml(inquirer, networkFlag) {
1318
1348
  const { description } = await inquirer.prompt([
1319
1349
  {
1320
1350
  type: "input",
@@ -1361,10 +1391,34 @@ async function promptYaml(inquirer) {
1361
1391
  }
1362
1392
  }
1363
1393
  ]);
1394
+ let walletNetwork = "devnet";
1364
1395
  if (solanaAddress) {
1365
- console.log(
1366
- " The wallet receives every asset on Solana (SOL directly, USDC and other\n SPL tokens via their ATA). Each skill declares its own price and token\n in SKILL.md. Fund with SOL via `solana airdrop` or USDC via https://faucet.circle.com."
1367
- );
1396
+ if (networkFlag !== void 0) {
1397
+ walletNetwork = networkFlag;
1398
+ } else {
1399
+ const { network } = await inquirer.prompt([
1400
+ {
1401
+ type: "list",
1402
+ name: "network",
1403
+ message: "Solana network (fixed at init - to go mainnet later, create a new agent):",
1404
+ choices: [
1405
+ { name: "devnet (test tokens - recommended to start)", value: "devnet" },
1406
+ { name: "mainnet (REAL funds)", value: "mainnet" }
1407
+ ],
1408
+ default: "devnet"
1409
+ }
1410
+ ]);
1411
+ walletNetwork = network;
1412
+ }
1413
+ if (walletNetwork === "mainnet") {
1414
+ console.log(
1415
+ " ! MAINNET: this wallet handles REAL funds - every skill price is real money\n and there is no faucet. Fund it with real SOL (and USDC if skills price in it).\n The network is fixed at init: to go back to devnet, create a new agent."
1416
+ );
1417
+ } else {
1418
+ console.log(
1419
+ " The wallet receives every asset on Solana (SOL directly, USDC and other\n SPL tokens via their ATA). Each skill declares its own price and token\n in SKILL.md. Fund with SOL via `solana airdrop` or USDC via https://faucet.circle.com."
1420
+ );
1421
+ }
1368
1422
  }
1369
1423
  const { llmProvider } = await inquirer.prompt([
1370
1424
  {
@@ -1389,7 +1443,7 @@ async function promptYaml(inquirer) {
1389
1443
  picture: picture || void 0,
1390
1444
  banner: banner || void 0,
1391
1445
  relays: [...RELAYS],
1392
- payments: solanaAddress ? [{ chain: "solana", network: "devnet", address: solanaAddress }] : [],
1446
+ payments: solanaAddress ? [{ chain: "solana", network: walletNetwork, address: solanaAddress }] : [],
1393
1447
  security: {}
1394
1448
  };
1395
1449
  if (llmProvider === "none") {
@@ -1450,15 +1504,24 @@ var WATCHDOG_PROBE_TIMEOUT_MS = 1e4;
1450
1504
  var WATCHDOG_SELF_PING_INTERVAL_MS = 10 * 60 * 1e3;
1451
1505
  var WATCHDOG_SELF_PING_TIMEOUT_MS = 15e3;
1452
1506
  var WATCHDOG_SLEEP_DETECT_MULTIPLIER = 2;
1453
- function getRpcUrl(_network) {
1507
+ function getRpcUrl(network) {
1454
1508
  const envUrl = process.env.SOLANA_RPC_URL;
1455
1509
  if (envUrl) {
1456
1510
  return envUrl;
1457
1511
  }
1458
- return "https://api.devnet.solana.com";
1512
+ return network === "mainnet" ? "https://api.mainnet-beta.solana.com" : "https://api.devnet.solana.com";
1459
1513
  }
1460
- async function fetchUsdcBalance(rpc, owner) {
1461
- const mint = USDC_SOLANA_DEVNET.mint;
1514
+ async function solanaLineFor(agent) {
1515
+ try {
1516
+ const { yaml } = await readAgentPublic(agent);
1517
+ const solana = yaml.payments.find((entry) => entry.chain === "solana");
1518
+ return solana ? ` | Solana: ${solana.address} (${solana.network})` : "";
1519
+ } catch {
1520
+ return "";
1521
+ }
1522
+ }
1523
+ async function fetchSplBalance(rpc, owner, asset) {
1524
+ const mint = asset.mint;
1462
1525
  if (!mint) {
1463
1526
  return 0n;
1464
1527
  }
@@ -1472,21 +1535,39 @@ async function fetchUsdcBalance(rpc, owner) {
1472
1535
  for (const entry of response.value) {
1473
1536
  const parsed = entry.account.data;
1474
1537
  const raw = parsed?.parsed?.info?.tokenAmount?.amount;
1475
- if (typeof raw === "string") {
1476
- total += BigInt(raw);
1538
+ if (typeof raw !== "string") {
1539
+ return null;
1477
1540
  }
1541
+ total += BigInt(raw);
1478
1542
  }
1479
1543
  return total;
1480
1544
  } catch {
1481
- return 0n;
1545
+ return null;
1482
1546
  }
1483
1547
  }
1548
+ function formatSplBalanceValue(asset, balance) {
1549
+ return balance === void 0 || balance === null ? "unavailable (balance read failed)" : formatAssetAmount(asset, balance);
1550
+ }
1551
+ async function fetchUsdcBalance(rpc, owner, network) {
1552
+ return fetchSplBalance(rpc, owner, resolveUsdcAsset(network));
1553
+ }
1484
1554
 
1485
1555
  // src/x402/constants.ts
1486
1556
  var X402_SOLANA_DEVNET_CAIP2 = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1";
1487
1557
  var X402_SOLANA_DEVNET_V1 = "solana-devnet";
1558
+ var X402_SOLANA_MAINNET_CAIP2 = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
1559
+ var X402_SOLANA_MAINNET_V1 = "solana";
1560
+ function x402SolanaNetworkIds(network) {
1561
+ return network === "mainnet" ? { caip2: X402_SOLANA_MAINNET_CAIP2, v1: X402_SOLANA_MAINNET_V1 } : { caip2: X402_SOLANA_DEVNET_CAIP2, v1: X402_SOLANA_DEVNET_V1 };
1562
+ }
1488
1563
  var X402_GET_INPUT_MAX_ENCODED_BYTES = 2048;
1489
1564
  var X402_MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
1565
+ var X402_ERROR_EXCERPT_CHARS = 400;
1566
+ var X402_MAX_ERROR_BODY_BYTES = 8 * 1024;
1567
+ var X402_ERROR_BODY_READ_MS = 5e3;
1568
+ var X402_MAX_ERROR_BODY_READS = 2048;
1569
+ var X402_MAX_CHALLENGE_HEADER_CHARS = 64 * 1024;
1570
+ var X402_MASKED_INPUT_MIN_CHARS = 8;
1490
1571
  var X402_MAX_CHALLENGE_BYTES = 256 * 1024;
1491
1572
  var X402_PROBE_TTL_MS = 3e4;
1492
1573
  var X402_MAX_PAID_ATTEMPTS = 2;
@@ -1494,8 +1575,6 @@ var X402_MAX_PAYMENT_SIGNATURES = 2 * X402_MAX_PAID_ATTEMPTS;
1494
1575
  var X402_FREE_RETRY_DELAYS_MS = [1e3, 3e3];
1495
1576
  var X402_REFUNDED_RETRIES = 1;
1496
1577
  var X402_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
1497
-
1498
- // src/x402/matcher.ts
1499
1578
  function requirementAmount(requirement) {
1500
1579
  const v1Amount = requirement.maxAmountRequired;
1501
1580
  const raw = requirement.amount ?? v1Amount;
@@ -1509,10 +1588,12 @@ function isAcceptableRequirement(requirement, rule) {
1509
1588
  return false;
1510
1589
  }
1511
1590
  const network = requirement.network;
1512
- if (network !== X402_SOLANA_DEVNET_CAIP2 && network !== X402_SOLANA_DEVNET_V1) {
1591
+ const acceptedIds = x402SolanaNetworkIds(rule.network);
1592
+ if (network !== acceptedIds.caip2 && network !== acceptedIds.v1) {
1513
1593
  return false;
1514
1594
  }
1515
- if (USDC_SOLANA_DEVNET.mint === void 0 || requirement.asset !== USDC_SOLANA_DEVNET.mint) {
1595
+ const usdcMint = resolveUsdcAsset(rule.network).mint;
1596
+ if (usdcMint === void 0 || requirement.asset !== usdcMint) {
1516
1597
  return false;
1517
1598
  }
1518
1599
  const amount = requirementAmount(requirement);
@@ -1697,7 +1778,6 @@ async function probePaymentRequiredCached(url, method, ttlMs) {
1697
1778
  // src/commands/x402-add.ts
1698
1779
  var BPS_DENOMINATOR = 1e4;
1699
1780
  var DEFAULT_MARGIN_BPS = 1e3;
1700
- var SOLANA_MAINNET_CAIP2 = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
1701
1781
  var CIRCLE_FAUCET_URL = "https://faucet.circle.com";
1702
1782
  function fail(message) {
1703
1783
  console.error(`
@@ -1782,7 +1862,7 @@ function buildSkillMd(input) {
1782
1862
  "Operator notes for this x402 bridge skill (the executor ignores this body):",
1783
1863
  "",
1784
1864
  `- Upstream: ${input.url} (${input.method})`,
1785
- `- Upstream quote at generation time: ${formatAssetAmount(USDC_SOLANA_DEVNET, input.quote)} (= x402_max_upstream, the signing ceiling)`,
1865
+ `- Upstream quote at generation time: ${formatAssetAmount(resolveUsdcAsset(input.network), input.quote)} (= x402_max_upstream, the signing ceiling)`,
1786
1866
  `- Price: ${input.priceDisplay} USDC (margin ${input.marginPercent}%, protocol fee ${input.feePercent}% at generation time)`,
1787
1867
  "- To reprice after an upstream change, re-run:",
1788
1868
  "",
@@ -1834,15 +1914,17 @@ function scanExistingSkills(skillsDir, candidateDTag, url) {
1834
1914
  }
1835
1915
  return scan;
1836
1916
  }
1837
- function describeUnacceptableAccepts(probe) {
1917
+ function describeUnacceptableAccepts(probe, agentNetwork) {
1838
1918
  const networks = [...new Set(probe.accepts.map((requirement) => String(requirement.network)))];
1839
1919
  if (networks.length > 0 && networks.every((network) => network.startsWith("eip155:"))) {
1840
1920
  return `the service accepts only EVM networks (${networks.join(", ")}) - not supported yet`;
1841
1921
  }
1842
- if (networks.some((network) => network === SOLANA_MAINNET_CAIP2 || network === "solana")) {
1843
- return "the service settles on Solana mainnet - elisym currently runs on devnet (mainnet is on the roadmap)";
1922
+ const otherNetwork = agentNetwork === "mainnet" ? "devnet" : "mainnet";
1923
+ const otherIds = x402SolanaNetworkIds(otherNetwork);
1924
+ if (networks.some((network) => network === otherIds.caip2 || network === otherIds.v1)) {
1925
+ return `the service settles on Solana ${otherNetwork} but this agent is bound to ${agentNetwork} (one agent = one network) - bridge it from a ${otherNetwork} agent instead` + (otherNetwork === "mainnet" ? " (create one: npx @elisym/cli init --network mainnet)" : "");
1844
1926
  }
1845
- return `no exact-scheme devnet-USDC requirement found (service accepts: ${networks.join(", ") || "nothing parseable"})`;
1927
+ return `no exact-scheme ${agentNetwork}-USDC requirement found (service accepts: ${networks.join(", ") || "nothing parseable"})`;
1846
1928
  }
1847
1929
  async function cmdX402Add(url, agentName, options) {
1848
1930
  const cwd = process.cwd();
@@ -1886,6 +1968,33 @@ async function cmdX402Add(url, agentName, options) {
1886
1968
  }
1887
1969
  const passphrase = process.env.ELISYM_PASSPHRASE;
1888
1970
  const loaded = await loadAgent(agentName, cwd, passphrase);
1971
+ const existingSolPayment = loaded.yaml.payments.find((entry) => entry.chain === "solana");
1972
+ let agentNetwork;
1973
+ if (existingSolPayment !== void 0) {
1974
+ agentNetwork = existingSolPayment.network;
1975
+ } else if (options.yes) {
1976
+ agentNetwork = "devnet";
1977
+ } else {
1978
+ const { network } = await inquirer.prompt([
1979
+ {
1980
+ type: "list",
1981
+ name: "network",
1982
+ message: "This agent has no Solana wallet entry yet. Which network should it operate on (fixed once set)?",
1983
+ choices: [
1984
+ { name: "devnet (test tokens - recommended to start)", value: "devnet" },
1985
+ { name: "mainnet (REAL funds)", value: "mainnet" }
1986
+ ],
1987
+ default: "devnet"
1988
+ }
1989
+ ]);
1990
+ agentNetwork = network;
1991
+ if (agentNetwork === "mainnet") {
1992
+ console.log(
1993
+ " ! MAINNET: the bridge float and its upstream payments are REAL funds.\n The network is fixed once written - to switch, create a new agent."
1994
+ );
1995
+ }
1996
+ }
1997
+ const usdcAsset = resolveUsdcAsset(agentNetwork);
1889
1998
  console.log(`
1890
1999
  Probing ${url} (${method}, unpaid)...`);
1891
2000
  let probe;
@@ -1898,10 +2007,13 @@ async function cmdX402Add(url, agentName, options) {
1898
2007
  const message = error instanceof Error ? error.message : String(error);
1899
2008
  fail(`upstream unreachable: ${message}`);
1900
2009
  }
1901
- const anyCeiling = { maxUpstreamSubunits: BigInt(Number.MAX_SAFE_INTEGER) };
2010
+ const anyCeiling = {
2011
+ maxUpstreamSubunits: BigInt(Number.MAX_SAFE_INTEGER),
2012
+ network: agentNetwork
2013
+ };
1902
2014
  const requirement = selectAcceptableRequirement(probe.accepts, anyCeiling);
1903
2015
  if (requirement === void 0) {
1904
- fail(describeUnacceptableAccepts(probe));
2016
+ fail(describeUnacceptableAccepts(probe, agentNetwork));
1905
2017
  }
1906
2018
  const quote = requirementAmount(requirement);
1907
2019
  if (quote === null || quote <= 0n) {
@@ -1963,6 +2075,11 @@ async function cmdX402Add(url, agentName, options) {
1963
2075
  );
1964
2076
  }
1965
2077
  }
2078
+ if (agentNetwork === "mainnet" && !passphrase) {
2079
+ console.warn(
2080
+ " ! No passphrase set: this MAINNET wallet key will be stored unencrypted on disk.\n Strongly consider one (ELISYM_PASSPHRASE) - the key will guard real funds."
2081
+ );
2082
+ }
1966
2083
  await writeSecrets(
1967
2084
  loaded.dir,
1968
2085
  { ...loaded.secrets, solana_secret_key: solanaSecretKey },
@@ -1971,33 +2088,35 @@ async function cmdX402Add(url, agentName, options) {
1971
2088
  console.log(" Wallet key saved to .secrets.json");
1972
2089
  }
1973
2090
  const signer = await signerFromSecretKeyBase58(solanaSecretKey);
1974
- const solPayment = loaded.yaml.payments.find((entry) => entry.chain === "solana");
1975
- if (solPayment !== void 0 && solPayment.address !== signer.address) {
2091
+ if (existingSolPayment !== void 0 && existingSolPayment.address !== signer.address) {
1976
2092
  fail(
1977
- `wallet invariant violated: elisym.yaml payments[].address (${solPayment.address}) differs from the solana_secret_key address (${signer.address}).
2093
+ `wallet invariant violated: elisym.yaml payments[].address (${existingSolPayment.address}) differs from the solana_secret_key address (${signer.address}).
1978
2094
  Revenue must land in the wallet the bridge spends from, or the float never refills.
1979
2095
  Fix: set payments[].address to the key address, or import the matching key.`
1980
2096
  );
1981
2097
  }
1982
- if (solPayment === void 0) {
2098
+ if (existingSolPayment === void 0) {
1983
2099
  console.log(
1984
- ` elisym.yaml has no Solana payments entry; adding address ${signer.address} (devnet).`
2100
+ ` elisym.yaml has no Solana payments entry; adding address ${signer.address} (${agentNetwork}).`
1985
2101
  );
1986
2102
  await writeYaml(loaded.dir, {
1987
2103
  ...loaded.yaml,
1988
2104
  payments: [
1989
2105
  ...loaded.yaml.payments,
1990
- { chain: "solana", network: "devnet", address: signer.address }
2106
+ { chain: "solana", network: agentNetwork, address: signer.address }
1991
2107
  ]
1992
2108
  });
1993
2109
  }
1994
- const rpc = createSolanaRpc(getRpcUrl());
1995
- const balance = await fetchUsdcBalance(rpc, address(signer.address));
2110
+ const rpc = createSolanaRpc(getRpcUrl(agentNetwork));
2111
+ const balance = await fetchUsdcBalance(rpc, address(signer.address), agentNetwork);
1996
2112
  let feeBps;
1997
2113
  try {
1998
- const protocolConfig = await getProtocolConfig(rpc, getProtocolProgramId("devnet"), {
1999
- forceRefresh: true
2000
- });
2114
+ const protocolConfig = await getProtocolConfig(
2115
+ rpc,
2116
+ getProtocolProgramId(agentNetwork),
2117
+ agentNetwork,
2118
+ { forceRefresh: true }
2119
+ );
2001
2120
  feeBps = protocolConfig.feeBps;
2002
2121
  } catch (error) {
2003
2122
  const message = error instanceof Error ? error.message : String(error);
@@ -2009,7 +2128,7 @@ async function cmdX402Add(url, agentName, options) {
2009
2128
  );
2010
2129
  }
2011
2130
  const priceSubunits = computeBridgePriceSubunits(quote, marginBps, feeBps);
2012
- const priceDisplay = new Decimal(priceSubunits.toString()).div(new Decimal(10).pow(USDC_SOLANA_DEVNET.decimals)).toString();
2131
+ const priceDisplay = new Decimal(priceSubunits.toString()).div(new Decimal(10).pow(usdcAsset.decimals)).toString();
2013
2132
  const protocolFee = calculateProtocolFee(Number(priceSubunits), feeBps);
2014
2133
  const netMargin = priceSubunits - BigInt(protocolFee) - quote;
2015
2134
  const fallbackName = slugFromUrl(parsedUrl);
@@ -2078,18 +2197,17 @@ async function cmdX402Add(url, agentName, options) {
2078
2197
  console.log(
2079
2198
  ` Upstream ${url} (${method}${queryParam !== void 0 ? `, input in ?${queryParam}=` : ""}${noInput ? ", no buyer input" : ""})`
2080
2199
  );
2081
- console.log(` Quote ${formatAssetAmount(USDC_SOLANA_DEVNET, quote)}`);
2082
- console.log(
2083
- ` Your price ${formatAssetAmount(USDC_SOLANA_DEVNET, priceSubunits)} (margin ${marginPercent}% + protocol fee ${feePercent}%)`
2084
- );
2085
- console.log(` Net margin ${formatAssetAmount(USDC_SOLANA_DEVNET, netMargin)} per job`);
2200
+ console.log(` Network ${agentNetwork}`);
2201
+ console.log(` Quote ${formatAssetAmount(usdcAsset, quote)}`);
2086
2202
  console.log(
2087
- ` Float ${formatAssetAmount(USDC_SOLANA_DEVNET, balance)} in ${signer.address}`
2203
+ ` Your price ${formatAssetAmount(usdcAsset, priceSubunits)} (margin ${marginPercent}% + protocol fee ${feePercent}%)`
2088
2204
  );
2089
- if (balance < quote) {
2205
+ console.log(` Net margin ${formatAssetAmount(usdcAsset, netMargin)} per job`);
2206
+ console.log(` Float ${formatSplBalanceValue(usdcAsset, balance)} in ${signer.address}`);
2207
+ if (balance !== null && balance < quote) {
2090
2208
  console.log(
2091
2209
  chalk.yellow(
2092
- ` ! Float below one upstream quote - fund the wallet with devnet USDC: ${CIRCLE_FAUCET_URL}`
2210
+ agentNetwork === "mainnet" ? " ! Float below one upstream quote - fund the wallet with USDC (mainnet, REAL funds)" : ` ! Float below one upstream quote - fund the wallet with devnet USDC: ${CIRCLE_FAUCET_URL}`
2093
2211
  )
2094
2212
  );
2095
2213
  }
@@ -2113,10 +2231,12 @@ async function cmdX402Add(url, agentName, options) {
2113
2231
  queryParam,
2114
2232
  quote,
2115
2233
  marginPercent,
2116
- feePercent
2234
+ feePercent,
2235
+ network: agentNetwork
2117
2236
  });
2118
2237
  const roundTrip = parseSkillMd(content);
2119
2238
  validateSkillFrontmatter(roundTrip.frontmatter, roundTrip.systemPrompt, {
2239
+ network: agentNetwork,
2120
2240
  allowFreeSkills: false,
2121
2241
  allowX402Skills: true
2122
2242
  });
@@ -2125,8 +2245,10 @@ async function cmdX402Add(url, agentName, options) {
2125
2245
  await ensureGitignoreHasX402Entries(dirname(loaded.dir));
2126
2246
  console.log(`
2127
2247
  Wrote ${join(targetDir, "SKILL.md")}`);
2128
- if (balance < quote) {
2129
- console.log(` Next: fund the float (devnet USDC): ${CIRCLE_FAUCET_URL}`);
2248
+ if (balance !== null && balance < quote) {
2249
+ console.log(
2250
+ agentNetwork === "mainnet" ? " Next: fund the float with USDC on mainnet (REAL funds)." : ` Next: fund the float (devnet USDC): ${CIRCLE_FAUCET_URL}`
2251
+ );
2130
2252
  }
2131
2253
  console.log(` Start the agent: npx @elisym/cli start ${agentName}
2132
2254
  `);
@@ -2162,7 +2284,7 @@ async function resolveAgentName(name, cwd, nonInteractive) {
2162
2284
  }
2163
2285
  async function printDelegateStatus(delegateSecretKey, network) {
2164
2286
  const signer = await signerFromSecretKeyBase58(delegateSecretKey);
2165
- const rpc = createSolanaRpc(getRpcUrl());
2287
+ const rpc = createSolanaRpc(getRpcUrl(network));
2166
2288
  let gasLine = "";
2167
2289
  try {
2168
2290
  const { value: balance } = await rpc.getBalance(address(signer.address)).send();
@@ -2186,7 +2308,7 @@ async function cmdDelegateKey(name, options) {
2186
2308
  const agentName = await resolveAgentName(name, cwd, options.yes === true);
2187
2309
  const passphrase = process.env.ELISYM_PASSPHRASE;
2188
2310
  const loaded = await loadAgent(agentName, cwd, passphrase);
2189
- loaded.yaml.payments.find((entry) => entry.chain === "solana")?.network ?? "devnet";
2311
+ const network = loaded.yaml.payments.find((entry) => entry.chain === "solana")?.network ?? "devnet";
2190
2312
  const existing = loaded.secrets.solana_delegate_secret_key;
2191
2313
  if (options.show) {
2192
2314
  if (!existing) {
@@ -2195,12 +2317,12 @@ async function cmdDelegateKey(name, options) {
2195
2317
  );
2196
2318
  process.exit(1);
2197
2319
  }
2198
- await printDelegateStatus(existing);
2320
+ await printDelegateStatus(existing, network);
2199
2321
  return;
2200
2322
  }
2201
2323
  if (existing && !options.rotate) {
2202
2324
  console.log(` Agent "${agentName}" already has a delegate key.`);
2203
- await printDelegateStatus(existing);
2325
+ await printDelegateStatus(existing, network);
2204
2326
  console.log(" Use --rotate to replace it (this invalidates outstanding owner approvals).");
2205
2327
  return;
2206
2328
  }
@@ -2257,6 +2379,11 @@ async function cmdDelegateKey(name, options) {
2257
2379
  process.exit(1);
2258
2380
  }
2259
2381
  }
2382
+ if (network === "mainnet" && !passphrase) {
2383
+ console.warn(
2384
+ " ! No passphrase set: this MAINNET delegate key will be stored unencrypted on disk.\n Strongly consider one (ELISYM_PASSPHRASE) - the key spends owner-approved real funds."
2385
+ );
2386
+ }
2260
2387
  await writeSecrets(
2261
2388
  loaded.dir,
2262
2389
  { ...loaded.secrets, solana_delegate_secret_key: delegateSecretKey },
@@ -2265,7 +2392,7 @@ async function cmdDelegateKey(name, options) {
2265
2392
  console.log(
2266
2393
  existing ? "\n Delegate key rotated and saved." : "\n Delegate key generated and saved."
2267
2394
  );
2268
- await printDelegateStatus(delegateSecretKey);
2395
+ await printDelegateStatus(delegateSecretKey, network);
2269
2396
  }
2270
2397
  var IDENTITY_PLATFORMS = ["github", "x", "website"];
2271
2398
  var X_PROOF_URL_HOSTS = /* @__PURE__ */ new Set([
@@ -2899,12 +3026,12 @@ async function cmdProfile(name) {
2899
3026
  console.log(" Profile updated.\n");
2900
3027
  }
2901
3028
  if (section === "wallet") {
2902
- const current = loaded.yaml.payments[0];
3029
+ const current = loaded.yaml.payments.find((entry) => entry.chain === "solana");
2903
3030
  const answers = await inquirer.prompt([
2904
3031
  {
2905
3032
  type: "input",
2906
3033
  name: "address",
2907
- message: "Solana address (empty to clear):",
3034
+ message: current ? "Solana address:" : "Solana address (empty to skip):",
2908
3035
  default: current?.address ?? "",
2909
3036
  validate: (value) => {
2910
3037
  if (!value) {
@@ -2914,9 +3041,40 @@ async function cmdProfile(name) {
2914
3041
  }
2915
3042
  }
2916
3043
  ]);
3044
+ if (!answers.address && current) {
3045
+ console.log(
3046
+ " ! The wallet entry cannot be removed: it pins the agent network, which is\n fixed at creation - to change networks, create a new agent. To stop\n charging, set every skill price to 0 instead. Wallet unchanged.\n"
3047
+ );
3048
+ continue;
3049
+ }
3050
+ let walletNetwork = current?.network;
3051
+ if (answers.address && walletNetwork === void 0) {
3052
+ const { network } = await inquirer.prompt([
3053
+ {
3054
+ type: "list",
3055
+ name: "network",
3056
+ message: "Solana network (fixed once set - to switch later, create a new agent):",
3057
+ choices: [
3058
+ { name: "devnet (test tokens - recommended to start)", value: "devnet" },
3059
+ { name: "mainnet (REAL funds)", value: "mainnet" }
3060
+ ],
3061
+ default: "devnet"
3062
+ }
3063
+ ]);
3064
+ walletNetwork = network;
3065
+ if (walletNetwork === "mainnet") {
3066
+ console.log(
3067
+ " ! MAINNET: this wallet handles REAL funds - every skill price is real money\n and there is no faucet. Strongly consider encrypting secrets with a passphrase."
3068
+ );
3069
+ }
3070
+ }
3071
+ const otherChainEntries = loaded.yaml.payments.filter((entry) => entry.chain !== "solana");
2917
3072
  const nextYaml = {
2918
3073
  ...loaded.yaml,
2919
- payments: answers.address ? [{ chain: "solana", network: "devnet", address: answers.address }] : []
3074
+ payments: answers.address && walletNetwork !== void 0 ? [
3075
+ ...otherChainEntries,
3076
+ { chain: "solana", network: walletNetwork, address: answers.address }
3077
+ ] : otherChainEntries
2920
3078
  };
2921
3079
  await writeYaml(loaded.dir, nextYaml);
2922
3080
  loaded.yaml = nextYaml;
@@ -4748,14 +4906,11 @@ var AgentRuntime = class {
4748
4906
  }
4749
4907
  /** Fetch on-chain protocol config (fee, treasury). Always fetches fresh to avoid stale treasury. */
4750
4908
  async fetchProtocolConfig() {
4751
- if (this.config.network !== "devnet") {
4752
- throw new Error(
4753
- `Network "${this.config.network}" is not supported. Only "devnet" is available until the on-chain protocol program is deployed on mainnet.`
4754
- );
4755
- }
4756
- const programId = getProtocolProgramId("devnet");
4909
+ const programId = getProtocolProgramId(this.config.network);
4757
4910
  const rpc = createSolanaRpc(getRpcUrl(this.config.network));
4758
- const config = await getProtocolConfig(rpc, programId, { forceRefresh: true });
4911
+ const config = await getProtocolConfig(rpc, programId, this.config.network, {
4912
+ forceRefresh: true
4913
+ });
4759
4914
  return { feeBps: config.feeBps, treasury: config.treasury };
4760
4915
  }
4761
4916
  async run() {
@@ -5243,12 +5398,6 @@ var AgentRuntime = class {
5243
5398
  "this agent does not accept delegated payment."
5244
5399
  );
5245
5400
  }
5246
- if (this.config.network !== "devnet") {
5247
- return reject(
5248
- `delegation is devnet-only (network: ${this.config.network})`,
5249
- "this agent does not accept delegated payment."
5250
- );
5251
- }
5252
5401
  const network = this.config.network;
5253
5402
  if (!skill || skill.delegation === void 0 || skill.priceSubunits <= 0) {
5254
5403
  return reject(
@@ -5365,7 +5514,9 @@ var AgentRuntime = class {
5365
5514
  // first delegated job does not dead-letter on a missing ATA.
5366
5515
  ensureDestination: { owner: this.config.solanaAddress }
5367
5516
  });
5368
- const pull = await buildSignedPull(ctx.rpc, ctx.delegateSigner, instructions);
5517
+ const pull = await buildSignedPull(ctx.rpc, ctx.delegateSigner, instructions, {
5518
+ network: ctx.network
5519
+ });
5369
5520
  this.ledger.recordPullSignature(
5370
5521
  job.jobId,
5371
5522
  pull.signature,
@@ -5840,6 +5991,7 @@ var AgentRuntime = class {
5840
5991
  this.config.solanaAddress,
5841
5992
  jobPrice,
5842
5993
  protocolConfig,
5994
+ this.config.network,
5843
5995
  { expirySecs: this.config.paymentTimeoutSecs, asset: jobAsset }
5844
5996
  );
5845
5997
  const requestJson = JSON.stringify(request);
@@ -6804,7 +6956,7 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
6804
6956
  }
6805
6957
  return skill;
6806
6958
  }
6807
- function loadSkillsFromDir(skillsDir, options = {}) {
6959
+ function loadSkillsFromDir(skillsDir, options) {
6808
6960
  const skills = [];
6809
6961
  let entries;
6810
6962
  try {
@@ -6826,10 +6978,16 @@ function loadSkillsFromDir(skillsDir, options = {}) {
6826
6978
  const content = readFileSync(skillMdPath, "utf-8");
6827
6979
  const { frontmatter, systemPrompt } = parseSkillMd(content);
6828
6980
  const parsed = validateSkillFrontmatter(frontmatter, systemPrompt, {
6981
+ network: options.network,
6829
6982
  allowFreeSkills: true,
6830
6983
  // The CLI runtime wires an x402 driver into the skill context, so
6831
6984
  // x402 skills are executable here (SDK-only hosts reject them).
6832
- allowX402Skills: true
6985
+ allowX402Skills: true,
6986
+ // Surface SDK load-time warnings (e.g. the devnet lsm->SOL pricing
6987
+ // fallback) in the start flow's ` !` console pattern.
6988
+ logger: {
6989
+ warn: (_obj, msg) => console.warn(` ! ${msg ?? ""}`)
6990
+ }
6833
6991
  });
6834
6992
  skills.push(buildCliSkill(parsed, entryPath, options.scriptEnv));
6835
6993
  } catch (e) {
@@ -7368,6 +7526,15 @@ function isTextContentType(contentType) {
7368
7526
  const mime = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
7369
7527
  return mime.startsWith("text/") || mime === "application/json" || mime.endsWith("+json");
7370
7528
  }
7529
+ function concatChunks(chunks, total) {
7530
+ const combined = new Uint8Array(total);
7531
+ let offset = 0;
7532
+ for (const chunk of chunks) {
7533
+ combined.set(chunk, offset);
7534
+ offset += chunk.byteLength;
7535
+ }
7536
+ return combined;
7537
+ }
7371
7538
  async function readBodyCapped(response, maxBytes) {
7372
7539
  const reader = response.body?.getReader();
7373
7540
  if (reader === void 0) {
@@ -7392,13 +7559,43 @@ async function readBodyCapped(response, maxBytes) {
7392
7559
  }
7393
7560
  chunks.push(value);
7394
7561
  }
7395
- const combined = new Uint8Array(total);
7396
- let offset = 0;
7397
- for (const chunk of chunks) {
7398
- combined.set(chunk, offset);
7399
- offset += chunk.byteLength;
7562
+ return concatChunks(chunks, total);
7563
+ }
7564
+ async function readBodyPrefix(response, maxBytes, timeoutMs) {
7565
+ const reader = response.body?.getReader();
7566
+ if (reader === void 0) {
7567
+ return { text: "", truncated: false };
7400
7568
  }
7401
- return combined;
7569
+ const chunks = [];
7570
+ let total = 0;
7571
+ let ended = false;
7572
+ let timedOut = false;
7573
+ const deadline = setTimeout(() => {
7574
+ timedOut = true;
7575
+ reader.cancel().catch(() => {
7576
+ });
7577
+ }, timeoutMs);
7578
+ try {
7579
+ for (let reads = 0; reads < X402_MAX_ERROR_BODY_READS && total < maxBytes; reads += 1) {
7580
+ const { done, value } = await reader.read();
7581
+ if (done) {
7582
+ ended = true;
7583
+ break;
7584
+ }
7585
+ const fitting = value.subarray(0, maxBytes - total);
7586
+ total += fitting.byteLength;
7587
+ chunks.push(fitting);
7588
+ }
7589
+ } catch {
7590
+ } finally {
7591
+ clearTimeout(deadline);
7592
+ }
7593
+ reader.cancel().catch(() => {
7594
+ });
7595
+ return {
7596
+ text: new TextDecoder().decode(concatChunks(chunks, total)),
7597
+ truncated: !ended || timedOut
7598
+ };
7402
7599
  }
7403
7600
  async function capChallengeResponse(response) {
7404
7601
  if (response.status !== 402) {
@@ -7411,6 +7608,105 @@ async function capChallengeResponse(response) {
7411
7608
  headers: response.headers
7412
7609
  });
7413
7610
  }
7611
+ var PAYMENT_IDENTIFIER_EXTENSION = "payment-identifier";
7612
+ function asRecord(value) {
7613
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
7614
+ }
7615
+ function extensionInfo(declaration) {
7616
+ if (declaration === null) {
7617
+ return {};
7618
+ }
7619
+ return Object.hasOwn(declaration, "info") ? asRecord(declaration.info) ?? {} : declaration;
7620
+ }
7621
+ function requiredPaymentIdentifier(response) {
7622
+ const header = response.headers.get("payment-required");
7623
+ if (header === null || header.length > X402_MAX_CHALLENGE_HEADER_CHARS) {
7624
+ return null;
7625
+ }
7626
+ try {
7627
+ const decoded = JSON.parse(Buffer.from(header, "base64").toString("utf8"));
7628
+ const extensions = asRecord(asRecord(decoded)?.extensions);
7629
+ const declaration = asRecord(extensions?.[PAYMENT_IDENTIFIER_EXTENSION]);
7630
+ return extensionInfo(declaration).required === true ? declaration : null;
7631
+ } catch {
7632
+ return null;
7633
+ }
7634
+ }
7635
+ function withPaymentIdentifier(headerValue, declaration, jobId) {
7636
+ try {
7637
+ const record = asRecord(JSON.parse(Buffer.from(headerValue, "base64").toString("utf8")));
7638
+ if (record === null) {
7639
+ return headerValue;
7640
+ }
7641
+ const transaction = asRecord(record.payload)?.transaction;
7642
+ const signed = typeof transaction === "string" ? transaction : headerValue;
7643
+ const digest = createHash("sha256").update(`${jobId}:${signed}`).digest("hex");
7644
+ const id = `pay_${digest.slice(0, 32)}`;
7645
+ const extensions = asRecord(record.extensions) ?? {};
7646
+ const echoed = asRecord(extensions[PAYMENT_IDENTIFIER_EXTENSION]) ?? {};
7647
+ const info = { id, ...extensionInfo(declaration), ...extensionInfo(echoed) };
7648
+ record.extensions = {
7649
+ ...extensions,
7650
+ // Mirror the shape the server declared. Its echo VALIDATOR would accept
7651
+ // either (`@x402/core` normalizes both sides through `getExtensionInfo`
7652
+ // first), but whatever reads the id back out afterwards is the server's
7653
+ // own code, and the one place it can be counted on to look is where it
7654
+ // declared the extension.
7655
+ [PAYMENT_IDENTIFIER_EXTENSION]: Object.hasOwn(declaration, "info") ? { ...echoed, info } : info
7656
+ };
7657
+ return Buffer.from(JSON.stringify(record), "utf8").toString("base64");
7658
+ } catch {
7659
+ return headerValue;
7660
+ }
7661
+ }
7662
+ var UNICODE_FORMAT_MARKS = /\p{Cf}/gu;
7663
+ function flattenForOperator(text) {
7664
+ return sanitizeForTerminal(text).replace(UNICODE_FORMAT_MARKS, "").replace(/\s+/g, " ").trim();
7665
+ }
7666
+ function printedPrefix(flattened) {
7667
+ return flattened.length <= X402_ERROR_EXCERPT_CHARS ? flattened : flattened.slice(0, X402_ERROR_EXCERPT_CHARS).replace(/[\ud800-\udbff]$/, "");
7668
+ }
7669
+ function clipForOperator(flattened) {
7670
+ return flattened.length <= X402_ERROR_EXCERPT_CHARS ? flattened : `${printedPrefix(flattened)}...`;
7671
+ }
7672
+ function quoteUpstream(text) {
7673
+ return clipForOperator(flattenForOperator(text));
7674
+ }
7675
+ function quoteUpstreamWithInput(text, sentInput) {
7676
+ return clipForOperator(maskCustomerInput(flattenForOperator(text), inputForms(sentInput)));
7677
+ }
7678
+ function wireForm(sentInput) {
7679
+ return new TextDecoder().decode(new TextEncoder().encode(sentInput));
7680
+ }
7681
+ function jsonEscapedForm(sentInput) {
7682
+ return JSON.stringify(sentInput).slice(1, -1);
7683
+ }
7684
+ function urlEncodedForms(sentInput) {
7685
+ const forms = [new URLSearchParams({ q: sentInput }).toString().slice("q=".length)];
7686
+ try {
7687
+ forms.push(encodeURIComponent(sentInput));
7688
+ } catch {
7689
+ }
7690
+ return forms;
7691
+ }
7692
+ function endsInsideInput(text, forms) {
7693
+ const tail = text.replace(/\ufffd+$/, "").slice(-X402_MASKED_INPUT_MIN_CHARS);
7694
+ return tail.length >= X402_MASKED_INPUT_MIN_CHARS && forms.some((form) => form.includes(tail));
7695
+ }
7696
+ function inputForms(sentInput) {
7697
+ return [wireForm(sentInput), jsonEscapedForm(sentInput), ...urlEncodedForms(sentInput)].map(
7698
+ flattenForOperator
7699
+ );
7700
+ }
7701
+ function maskCustomerInput(text, forms) {
7702
+ let masked = text;
7703
+ for (const form of forms) {
7704
+ if (form.length >= X402_MASKED_INPUT_MIN_CHARS) {
7705
+ masked = masked.replaceAll(form, "[input]");
7706
+ }
7707
+ }
7708
+ return masked;
7709
+ }
7414
7710
  function abortableDelay(ms, signal) {
7415
7711
  return new Promise((resolve4) => {
7416
7712
  if (signal?.aborted) {
@@ -7433,15 +7729,17 @@ var X402Driver = class {
7433
7729
  this.options = options;
7434
7730
  this.store = new X402JobStore(options.agentDir);
7435
7731
  this.rpc = createSolanaRpc(options.rpcUrl);
7732
+ this.usdcAsset = resolveUsdcAsset(options.network);
7436
7733
  this.store.sweepExpired().catch(() => {
7437
7734
  });
7438
7735
  }
7439
7736
  store;
7440
7737
  rpc;
7738
+ usdcAsset;
7441
7739
  signerPromise = null;
7442
7740
  repriceHintLogged = /* @__PURE__ */ new Set();
7443
7741
  log(message) {
7444
- (this.options.log ?? console.log)(`[x402] ${message}`);
7742
+ (this.options.log ?? console.log)(`[x402] ${flattenForOperator(message)}`);
7445
7743
  }
7446
7744
  getSigner() {
7447
7745
  const secret = this.options.solanaSecretKeyBase58;
@@ -7498,13 +7796,16 @@ var X402Driver = class {
7498
7796
  return;
7499
7797
  }
7500
7798
  const signer = await this.assertWalletInvariant();
7501
- if (job.asset.mint !== USDC_SOLANA_DEVNET.mint || USDC_SOLANA_DEVNET.mint === void 0) {
7799
+ if (job.asset.mint !== this.usdcAsset.mint || this.usdcAsset.mint === void 0) {
7502
7800
  throw new X402PreflightError(
7503
- `x402 skills must be priced in devnet USDC (skill "${job.skillName}" is priced in ${job.asset.symbol}); the margin check compares the elisym price to the upstream USDC quote - re-run \`npx @elisym/cli x402 add\``
7801
+ `x402 skills must be priced in ${this.options.network} USDC (skill "${job.skillName}" is priced in ${job.asset.symbol}); the margin check compares the elisym price to the upstream USDC quote - re-run \`npx @elisym/cli x402 add\``
7504
7802
  );
7505
7803
  }
7506
7804
  this.assertInputRules(job, input);
7507
- const rule = { maxUpstreamSubunits: job.params.maxUpstreamSubunits };
7805
+ const rule = {
7806
+ maxUpstreamSubunits: job.params.maxUpstreamSubunits,
7807
+ network: this.options.network
7808
+ };
7508
7809
  let probe;
7509
7810
  try {
7510
7811
  probe = await probePaymentRequiredCached(
@@ -7514,10 +7815,10 @@ var X402Driver = class {
7514
7815
  );
7515
7816
  } catch (error) {
7516
7817
  if (error instanceof X402ProbeError) {
7517
- throw new X402PreflightError(`upstream probe failed: ${error.message}`);
7818
+ throw new X402PreflightError(`upstream probe failed: ${quoteUpstream(error.message)}`);
7518
7819
  }
7519
7820
  const message = error instanceof Error ? error.message : String(error);
7520
- throw new X402PreflightError(`upstream unreachable: ${message}`);
7821
+ throw new X402PreflightError(`upstream unreachable: ${quoteUpstream(message)}`);
7521
7822
  }
7522
7823
  const quote = maxAcceptableQuote(probe.accepts, rule);
7523
7824
  if (quote === null) {
@@ -7528,13 +7829,18 @@ var X402Driver = class {
7528
7829
  if (quote < job.params.maxUpstreamSubunits && !this.repriceHintLogged.has(job.skillName)) {
7529
7830
  this.repriceHintLogged.add(job.skillName);
7530
7831
  this.log(
7531
- `upstream for "${job.skillName}" now quotes ${formatAssetAmount(USDC_SOLANA_DEVNET, quote)} (your price was computed from ${formatAssetAmount(USDC_SOLANA_DEVNET, job.params.maxUpstreamSubunits)}); re-run \`npx @elisym/cli x402 add\` to reprice`
7832
+ `upstream for "${job.skillName}" now quotes ${formatAssetAmount(this.usdcAsset, quote)} (your price was computed from ${formatAssetAmount(this.usdcAsset, job.params.maxUpstreamSubunits)}); re-run \`npx @elisym/cli x402 add\` to reprice`
7833
+ );
7834
+ }
7835
+ const balance = await fetchUsdcBalance(this.rpc, address(signer.address), this.options.network);
7836
+ if (balance === null) {
7837
+ throw new X402PreflightError(
7838
+ `could not read the bridge float balance for ${signer.address} - refusing to pay the upstream without verifying it; retry in a moment`
7532
7839
  );
7533
7840
  }
7534
- const balance = await fetchUsdcBalance(this.rpc, address(signer.address));
7535
7841
  if (balance < quote) {
7536
7842
  throw new X402PreflightError(
7537
- `bridge float insufficient: ${formatAssetAmount(USDC_SOLANA_DEVNET, balance)} USDC in ${signer.address}, upstream quotes ${formatAssetAmount(USDC_SOLANA_DEVNET, quote)} - top up the wallet`
7843
+ `bridge float insufficient: ${formatAssetAmount(this.usdcAsset, balance)} USDC in ${signer.address}, upstream quotes ${formatAssetAmount(this.usdcAsset, quote)} - top up the wallet`
7538
7844
  );
7539
7845
  }
7540
7846
  const feeBps = await this.options.getFeeBps();
@@ -7542,7 +7848,7 @@ var X402Driver = class {
7542
7848
  const net = BigInt(job.priceSubunits - fee);
7543
7849
  if (net < quote) {
7544
7850
  throw new X402PreflightError(
7545
- `negative margin: net revenue ${formatAssetAmount(USDC_SOLANA_DEVNET, net)} (price minus ${feeBps} bps protocol fee) is below the upstream quote ${formatAssetAmount(USDC_SOLANA_DEVNET, quote)} - re-run \`npx @elisym/cli x402 add\` to reprice`
7851
+ `negative margin: net revenue ${formatAssetAmount(this.usdcAsset, net)} (price minus ${feeBps} bps protocol fee) is below the upstream quote ${formatAssetAmount(this.usdcAsset, quote)} - re-run \`npx @elisym/cli x402 add\` to reprice`
7546
7852
  );
7547
7853
  }
7548
7854
  }
@@ -7558,6 +7864,7 @@ var X402Driver = class {
7558
7864
  */
7559
7865
  buildInstrumentedFetch(jobId, attemptState) {
7560
7866
  const store = this.store;
7867
+ let requiredIdentifier = null;
7561
7868
  return async function instrumentedFetch(info, init) {
7562
7869
  const request = new Request(info, init);
7563
7870
  if (request.headers.has("PAYMENT-SIGNATURE") || request.headers.has("X-PAYMENT")) {
@@ -7572,19 +7879,30 @@ var X402Driver = class {
7572
7879
  );
7573
7880
  }
7574
7881
  attemptState.paymentSent = true;
7882
+ const signature = request.headers.get("PAYMENT-SIGNATURE");
7883
+ if (requiredIdentifier !== null && signature !== null) {
7884
+ request.headers.set(
7885
+ "PAYMENT-SIGNATURE",
7886
+ withPaymentIdentifier(signature, requiredIdentifier, jobId)
7887
+ );
7888
+ }
7575
7889
  }
7576
7890
  const response = await globalThis.fetch(new Request(request, { redirect: "error" }));
7891
+ if (response.status === 402) {
7892
+ requiredIdentifier = requiredPaymentIdentifier(response);
7893
+ }
7577
7894
  return capChallengeResponse(response);
7578
7895
  };
7579
7896
  }
7580
7897
  buildClient(signer, rule) {
7581
7898
  const scheme = new ExactSvmScheme(signer, { rpcUrl: this.options.rpcUrl });
7899
+ const networkIds = x402SolanaNetworkIds(this.options.network);
7582
7900
  return x402Client.fromConfig({
7583
- schemes: [{ network: X402_SOLANA_DEVNET_CAIP2, client: scheme }],
7901
+ schemes: [{ network: networkIds.caip2, client: scheme }],
7584
7902
  policies: [buildRequirementsPolicy(rule)]
7585
- }).registerV1(X402_SOLANA_DEVNET_V1, scheme);
7903
+ }).registerV1(networkIds.v1, scheme);
7586
7904
  }
7587
- classifyWrapperError(error) {
7905
+ classifyWrapperError(error, sentInput) {
7588
7906
  if (error instanceof Error && error.name === "AbortError") {
7589
7907
  return error;
7590
7908
  }
@@ -7592,21 +7910,63 @@ var X402Driver = class {
7592
7910
  return error;
7593
7911
  }
7594
7912
  const message = error instanceof Error ? error.message : String(error);
7913
+ const quoted = quoteUpstreamWithInput(message, sentInput);
7595
7914
  if (message.includes("Failed to create payment payload")) {
7596
7915
  return new X402PermanentError(
7597
- `payment refused by policy (upstream likely repriced above x402_max_upstream): ${message}`
7916
+ `payment refused by policy (upstream likely repriced above x402_max_upstream): ${quoted}`
7598
7917
  );
7599
7918
  }
7600
- return new X402TransientError(`upstream request failed: ${message}`);
7919
+ return new X402TransientError(`upstream request failed: ${quoted}`);
7601
7920
  }
7602
- classifyStatus(status) {
7921
+ /**
7922
+ * Read a short, safe excerpt of a failing upstream's body.
7923
+ *
7924
+ * A bare status code is not a diagnosis: a 4xx from an x402 upstream can
7925
+ * mean a rejected payload, a missing required extension, or an argument the
7926
+ * service did not like, and the operator cannot tell which without the
7927
+ * body's own words. The body is untrusted, so only a bounded prefix is read
7928
+ * at all, and it is stripped of anything that could forge a log line before
7929
+ * it reaches the operator's. Reading it never fails the caller - an
7930
+ * unreadable body simply yields no excerpt, which is exactly the
7931
+ * status-only message as before.
7932
+ *
7933
+ * `sentInput` is the customer's own job input, masked out of the quote by
7934
+ * `maskCustomerInput`: an upstream that echoes the request back would
7935
+ * otherwise put it in the operator's log, which the runtime keeps it out of
7936
+ * everywhere else.
7937
+ */
7938
+ async errorExcerpt(response, sentInput) {
7939
+ try {
7940
+ const body = await readBodyPrefix(
7941
+ response,
7942
+ X402_MAX_ERROR_BODY_BYTES,
7943
+ this.options.errorBodyReadMs ?? X402_ERROR_BODY_READ_MS
7944
+ );
7945
+ const forms = inputForms(sentInput);
7946
+ const quoted = maskCustomerInput(flattenForOperator(body.text), forms);
7947
+ if (quoted.length === 0) {
7948
+ return "";
7949
+ }
7950
+ const shown = printedPrefix(quoted);
7951
+ if (body.truncated && endsInsideInput(shown, forms)) {
7952
+ return "";
7953
+ }
7954
+ return `: ${clipForOperator(quoted)}`;
7955
+ } catch {
7956
+ return "";
7957
+ }
7958
+ }
7959
+ classifyStatus(status, excerpt) {
7603
7960
  if (status === 402) {
7604
- return new X402TransientError("upstream still returned 402 after a payment attempt", status);
7961
+ return new X402TransientError(
7962
+ `upstream still returned 402 after a payment attempt${excerpt}`,
7963
+ status
7964
+ );
7605
7965
  }
7606
7966
  if (status === 429 || status >= 500) {
7607
- return new X402TransientError(`upstream returned ${status}`, status);
7967
+ return new X402TransientError(`upstream returned ${status}${excerpt}`, status);
7608
7968
  }
7609
- return new X402PermanentError(`upstream returned ${status}`);
7969
+ return new X402PermanentError(`upstream returned ${status}${excerpt}`);
7610
7970
  }
7611
7971
  async execute(job, input, signal) {
7612
7972
  const cached = await this.store.getResult(input.jobId);
@@ -7637,7 +7997,10 @@ var X402Driver = class {
7637
7997
  headers["content-type"] = jsonContentType(input.data) ? "application/json" : "text/plain";
7638
7998
  }
7639
7999
  const signer = await this.assertWalletInvariant();
7640
- const rule = { maxUpstreamSubunits: job.params.maxUpstreamSubunits };
8000
+ const rule = {
8001
+ maxUpstreamSubunits: job.params.maxUpstreamSubunits,
8002
+ network: this.options.network
8003
+ };
7641
8004
  const client = this.buildClient(signer, rule);
7642
8005
  const jobTag = input.jobId.slice(0, 8);
7643
8006
  let freeRetriesUsed = 0;
@@ -7650,7 +8013,7 @@ var X402Driver = class {
7650
8013
  requestUrl,
7651
8014
  headers,
7652
8015
  body,
7653
- input.jobId,
8016
+ input,
7654
8017
  client,
7655
8018
  attemptState,
7656
8019
  signal
@@ -7698,9 +8061,9 @@ var X402Driver = class {
7698
8061
  }
7699
8062
  }
7700
8063
  /** One full payment-wrapper pass against the upstream; throws classified errors. */
7701
- async attemptUpstream(job, requestUrl, headers, body, jobId, client, attemptState, signal) {
8064
+ async attemptUpstream(job, requestUrl, headers, body, input, client, attemptState, signal) {
7702
8065
  const fetchWithPayment = wrapFetchWithPayment(
7703
- this.buildInstrumentedFetch(jobId, attemptState),
8066
+ this.buildInstrumentedFetch(input.jobId, attemptState),
7704
8067
  client
7705
8068
  );
7706
8069
  let response;
@@ -7712,27 +8075,25 @@ var X402Driver = class {
7712
8075
  signal
7713
8076
  });
7714
8077
  } catch (error) {
7715
- throw this.classifyWrapperError(error);
8078
+ throw this.classifyWrapperError(error, input.data);
7716
8079
  }
7717
8080
  if (!response.ok) {
7718
- await response.body?.cancel().catch(() => {
7719
- });
7720
- throw this.classifyStatus(response.status);
8081
+ throw this.classifyStatus(response.status, await this.errorExcerpt(response, input.data));
7721
8082
  }
7722
8083
  let bytes;
7723
8084
  try {
7724
8085
  bytes = await readBodyCapped(response, X402_MAX_RESPONSE_BYTES);
7725
8086
  } catch (error) {
7726
- throw this.classifyWrapperError(error);
8087
+ throw this.classifyWrapperError(error, input.data);
7727
8088
  }
7728
8089
  const contentType = response.headers.get("content-type") ?? "application/octet-stream";
7729
8090
  if (isTextContentType(contentType)) {
7730
8091
  const data = new TextDecoder().decode(bytes);
7731
- await this.store.saveTextResult(jobId, data);
8092
+ await this.store.saveTextResult(input.jobId, data);
7732
8093
  return { data };
7733
8094
  }
7734
8095
  const mime = contentType.split(";")[0]?.trim() ?? "application/octet-stream";
7735
- const filePath = await this.store.saveFileResult(jobId, mime, bytes);
8096
+ const filePath = await this.store.saveFileResult(input.jobId, mime, bytes);
7736
8097
  return { data: "", outputMime: mime, filePath };
7737
8098
  }
7738
8099
  };
@@ -7770,9 +8131,10 @@ async function cmdStart(nameArg, options = {}) {
7770
8131
  const rpcUrl = getRpcUrl(walletNetwork);
7771
8132
  const rpc = createSolanaRpc(rpcUrl);
7772
8133
  const walletAddress = address(solanaAddress);
7773
- const [{ value: balanceLamports }, usdcBalance] = await Promise.all([
8134
+ const splAssets = splAssetsForNetwork(walletNetwork);
8135
+ const [{ value: balanceLamports }, ...splBalances] = await Promise.all([
7774
8136
  rpc.getBalance(walletAddress).send(),
7775
- fetchUsdcBalance(rpc, walletAddress)
8137
+ ...splAssets.map((asset) => fetchSplBalance(rpc, walletAddress, asset))
7776
8138
  ]);
7777
8139
  const balance = Number(balanceLamports);
7778
8140
  console.log(" Wallet");
@@ -7782,9 +8144,16 @@ async function cmdStart(nameArg, options = {}) {
7782
8144
  console.log(` RPC ${stripRpcSecrets(process.env.SOLANA_RPC_URL)} (custom)`);
7783
8145
  }
7784
8146
  console.log(` SOL ${formatSol(balance)} (${balance} lamports)`);
7785
- console.log(` USDC ${formatAssetAmount(USDC_SOLANA_DEVNET, usdcBalance)}`);
8147
+ for (const [index, asset] of splAssets.entries()) {
8148
+ const symbolColumn = asset.symbol.padEnd(8);
8149
+ console.log(` ${symbolColumn} ${formatSplBalanceValue(asset, splBalances[index])}`);
8150
+ }
7786
8151
  if (balance === 0) {
7787
- console.log(" ! Wallet is empty. Get devnet SOL: https://faucet.solana.com");
8152
+ if (walletNetwork === "mainnet") {
8153
+ console.log(" ! Wallet is empty. Fund it with real SOL to operate on mainnet.");
8154
+ } else {
8155
+ console.log(" ! Wallet is empty. Get devnet SOL: https://faucet.solana.com");
8156
+ }
7788
8157
  }
7789
8158
  console.log();
7790
8159
  } catch (e) {
@@ -7793,10 +8162,15 @@ async function cmdStart(nameArg, options = {}) {
7793
8162
  `);
7794
8163
  }
7795
8164
  }
8165
+ if (walletNetwork === "mainnet") {
8166
+ console.log(
8167
+ " ! Mainnet: per-skill prices are REAL funds. Review each SKILL.md price - skills copied from a devnet agent keep their absolute amounts.\n"
8168
+ );
8169
+ }
7796
8170
  const scriptEnv = buildScriptEnv(loaded.secrets);
7797
8171
  const paths = agentPaths(loaded.dir);
7798
8172
  const skillsDir = paths.skills;
7799
- const allSkills = loadSkillsFromDir(skillsDir, { scriptEnv });
8173
+ const allSkills = loadSkillsFromDir(skillsDir, { network: walletNetwork, scriptEnv });
7800
8174
  if (allSkills.length === 0) {
7801
8175
  console.error(` ! No skills found in ${skillsDir}
7802
8176
  `);
@@ -8009,9 +8383,9 @@ async function cmdStart(nameArg, options = {}) {
8009
8383
  }
8010
8384
  }
8011
8385
  for (const skill of x402Skills) {
8012
- if (skill.asset.mint !== USDC_SOLANA_DEVNET.mint) {
8386
+ if (skill.asset.mint !== resolveUsdcAsset(walletNetwork).mint) {
8013
8387
  console.warn(
8014
- ` ! x402 skill "${skill.name}" is priced in ${skill.asset.symbol}, not devnet USDC - every job will be refused at preflight. Re-run \`npx @elisym/cli x402 add\`.`
8388
+ ` ! x402 skill "${skill.name}" is priced in ${skill.asset.symbol}, not ${walletNetwork} USDC - every job will be refused at preflight. Re-run \`npx @elisym/cli x402 add\`.`
8015
8389
  );
8016
8390
  }
8017
8391
  }
@@ -8021,24 +8395,27 @@ async function cmdStart(nameArg, options = {}) {
8021
8395
  " ! x402 skills will NOT be advertised and their jobs will be refused before payment."
8022
8396
  );
8023
8397
  console.warn(" ! Fix: re-run `npx @elisym/cli x402 add` or align elisym.yaml with the key.");
8398
+ } else {
8399
+ const x402RpcUrl = getRpcUrl(walletNetwork);
8400
+ async function fetchLiveFeeBps() {
8401
+ const config = await getProtocolConfig(
8402
+ createSolanaRpc(x402RpcUrl),
8403
+ getProtocolProgramId(walletNetwork),
8404
+ walletNetwork,
8405
+ { forceRefresh: true }
8406
+ );
8407
+ return config.feeBps;
8408
+ }
8409
+ skillCtx.x402Driver = new X402Driver({
8410
+ agentDir: loaded.dir,
8411
+ paymentsAddress: solanaAddress,
8412
+ solanaSecretKeyBase58: loaded.secrets.solana_secret_key,
8413
+ rpcUrl: x402RpcUrl,
8414
+ network: walletNetwork,
8415
+ getFeeBps: fetchLiveFeeBps,
8416
+ log: (message) => console.log(` ${message}`)
8417
+ });
8024
8418
  }
8025
- const x402RpcUrl = getRpcUrl();
8026
- async function fetchLiveFeeBps() {
8027
- const config = await getProtocolConfig(
8028
- createSolanaRpc(x402RpcUrl),
8029
- getProtocolProgramId("devnet"),
8030
- { forceRefresh: true }
8031
- );
8032
- return config.feeBps;
8033
- }
8034
- skillCtx.x402Driver = new X402Driver({
8035
- agentDir: loaded.dir,
8036
- paymentsAddress: solanaAddress,
8037
- solanaSecretKeyBase58: loaded.secrets.solana_secret_key,
8038
- rpcUrl: x402RpcUrl,
8039
- getFeeBps: fetchLiveFeeBps,
8040
- log: (message) => console.log(` ${message}`)
8041
- });
8042
8419
  }
8043
8420
  console.log(" Connecting to relays and publishing capabilities...");
8044
8421
  const identity2 = ElisymIdentity.fromHex(loaded.secrets.nostr_secret_key);
@@ -8216,11 +8593,7 @@ async function cmdStart(nameArg, options = {}) {
8216
8593
  const declaresDelegation = allSkills.some((skill) => skill.delegation !== void 0);
8217
8594
  let delegatePubkey;
8218
8595
  let delegateSigner;
8219
- if (declaresDelegation && walletNetwork !== "devnet") {
8220
- console.warn(
8221
- ` ! A skill declares delegation but it is devnet-only today (network: ${walletNetwork}) - omitted from its card(s).`
8222
- );
8223
- } else if (declaresDelegation) {
8596
+ if (declaresDelegation) {
8224
8597
  const delegateSecret = loaded.secrets.solana_delegate_secret_key;
8225
8598
  if (delegateSecret && delegateSecret.length > 0) {
8226
8599
  try {
@@ -8231,7 +8604,7 @@ async function cmdStart(nameArg, options = {}) {
8231
8604
  const { value: delegateLamports } = await createSolanaRpc(getRpcUrl(walletNetwork)).getBalance(address(delegatePubkey)).send();
8232
8605
  if (delegateLamports === 0n) {
8233
8606
  console.warn(
8234
- " ! Delegate key holds no SOL - delegated job pulls will fail on tx fees. Fund it: https://faucet.solana.com"
8607
+ " ! Delegate key holds no SOL - delegated job pulls will fail on tx fees. " + (walletNetwork === "mainnet" ? "Fund it with real SOL." : "Fund it: https://faucet.solana.com")
8235
8608
  );
8236
8609
  }
8237
8610
  } catch {
@@ -8253,38 +8626,7 @@ async function cmdStart(nameArg, options = {}) {
8253
8626
  console.warn(` ! Run \`npx @elisym/cli delegate-key ${loaded.name}\` to enable it.`);
8254
8627
  }
8255
8628
  }
8256
- function buildCard(skill) {
8257
- const isStatic = skill.mode === "static-file" || skill.mode === "static-script" || skill.noInput === true;
8258
- return {
8259
- name: skill.name,
8260
- description: skill.description,
8261
- capabilities: skill.capabilities,
8262
- image: skill.image,
8263
- ...isStatic ? { static: true } : {},
8264
- // File-exchange hints (dynamic-script only). `inputMime` flags a file input;
8265
- // `inputText` tells the web whether to also show its text box for that file job.
8266
- ...skill.inputMime ? { inputMime: skill.inputMime } : {},
8267
- ...skill.inputText ? { inputText: skill.inputText } : {},
8268
- ...skill.outputMime ? { outputMime: skill.outputMime } : {},
8269
- // Conversation-context flag: clients gate chat affordances (session-carrying
8270
- // sends) on it. Strict true only - the read side coerces anything else away.
8271
- ...skill.context === true ? { context: true } : {},
8272
- // Delegation descriptor: only when the skill opts in AND a delegate key
8273
- // exists (delegatePubkey resolved). The pubkey is injected here, never
8274
- // taken from the SKILL.md - the operator declares only the cap/mechanism.
8275
- ...skill.delegation && delegatePubkey ? { delegation: { ...skill.delegation, delegate_pubkey: delegatePubkey } } : {},
8276
- payment: solanaAddress ? {
8277
- chain: "solana",
8278
- network: walletNetwork,
8279
- address: solanaAddress,
8280
- job_price: skill.priceSubunits,
8281
- token: skill.asset.token,
8282
- ...skill.asset.mint ? { mint: skill.asset.mint } : {},
8283
- decimals: skill.asset.decimals,
8284
- symbol: skill.asset.symbol
8285
- } : void 0
8286
- };
8287
- }
8629
+ const buildCard = (skill) => buildCapabilityCard(skill, { walletNetwork, solanaAddress, delegatePubkey });
8288
8630
  function retiredModelKeyForSkill(skill) {
8289
8631
  if (skill.resolvedTriple) {
8290
8632
  return `${skill.resolvedTriple.provider}::${skill.resolvedTriple.model}`;
@@ -8391,7 +8733,7 @@ async function cmdStart(nameArg, options = {}) {
8391
8733
  if (nonceStore !== void 0) {
8392
8734
  await ensureGitignoreHasDelegationNoncesEntry(dirname(loaded.dir));
8393
8735
  }
8394
- const rpcUrlForLog = stripRpcSecrets(process.env.SOLANA_RPC_URL ?? getRpcUrl());
8736
+ const rpcUrlForLog = stripRpcSecrets(process.env.SOLANA_RPC_URL ?? getRpcUrl(walletNetwork));
8395
8737
  logger.debug(
8396
8738
  {
8397
8739
  event: "config_resolved",
@@ -8469,6 +8811,39 @@ async function cmdStart(nameArg, options = {}) {
8469
8811
  console.log(" * Running. Press Ctrl+C to stop.\n");
8470
8812
  await runtime.run();
8471
8813
  }
8814
+ function buildCapabilityCard(skill, inputs) {
8815
+ const { walletNetwork, solanaAddress, delegatePubkey } = inputs;
8816
+ const isStatic = skill.mode === "static-file" || skill.mode === "static-script" || skill.noInput === true;
8817
+ return {
8818
+ name: skill.name,
8819
+ description: skill.description,
8820
+ capabilities: skill.capabilities,
8821
+ image: skill.image,
8822
+ ...isStatic ? { static: true } : {},
8823
+ // File-exchange hints (dynamic-script only). `inputMime` flags a file input;
8824
+ // `inputText` tells the web whether to also show its text box for that file job.
8825
+ ...skill.inputMime ? { inputMime: skill.inputMime } : {},
8826
+ ...skill.inputText ? { inputText: skill.inputText } : {},
8827
+ ...skill.outputMime ? { outputMime: skill.outputMime } : {},
8828
+ // Conversation-context flag: clients gate chat affordances (session-carrying
8829
+ // sends) on it. Strict true only - the read side coerces anything else away.
8830
+ ...skill.context === true ? { context: true } : {},
8831
+ // Delegation descriptor: only when the skill opts in AND a delegate key
8832
+ // exists (delegatePubkey resolved). The pubkey is injected here, never
8833
+ // taken from the SKILL.md - the operator declares only the cap/mechanism.
8834
+ ...skill.delegation && delegatePubkey ? { delegation: { ...skill.delegation, delegate_pubkey: delegatePubkey } } : {},
8835
+ payment: solanaAddress ? {
8836
+ chain: "solana",
8837
+ network: walletNetwork,
8838
+ address: solanaAddress,
8839
+ job_price: skill.priceSubunits,
8840
+ token: skill.asset.token,
8841
+ ...skill.asset.mint ? { mint: skill.asset.mint } : {},
8842
+ decimals: skill.asset.decimals,
8843
+ symbol: skill.asset.symbol
8844
+ } : void 0
8845
+ };
8846
+ }
8472
8847
  function buildScriptEnv(secrets) {
8473
8848
  const scriptEnv = { ...process.env };
8474
8849
  delete scriptEnv.ELISYM_PASSPHRASE;
@@ -8689,15 +9064,20 @@ async function cmdWallet(name) {
8689
9064
  const rpcUrl = getRpcUrl(solPayment.network);
8690
9065
  const rpc = createSolanaRpc(rpcUrl);
8691
9066
  const walletAddress = address(solPayment.address);
8692
- const { value: balance } = await rpc.getBalance(walletAddress).send();
8693
- const usdcBalance = await fetchUsdcBalance(rpc, walletAddress);
9067
+ const splAssets = splAssetsForNetwork(solPayment.network);
9068
+ const [{ value: balance }, ...splBalances] = await Promise.all([
9069
+ rpc.getBalance(walletAddress).send(),
9070
+ ...splAssets.map((asset) => fetchSplBalance(rpc, walletAddress, asset))
9071
+ ]);
8694
9072
  console.log(`
8695
9073
  Agent: ${name}`);
8696
9074
  console.log(` Network: ${solPayment.network}`);
8697
9075
  console.log(` Address: ${solPayment.address}`);
8698
9076
  console.log(` SOL balance: ${formatSol(Number(balance))} (${balance} lamports)`);
8699
- console.log(` USDC balance: ${formatAssetAmount(USDC_SOLANA_DEVNET, usdcBalance)}
8700
- `);
9077
+ for (const [index, asset] of splAssets.entries()) {
9078
+ console.log(` ${asset.symbol} balance: ${formatSplBalanceValue(asset, splBalances[index])}`);
9079
+ }
9080
+ console.log();
8701
9081
  }
8702
9082
  function readPackageVersion() {
8703
9083
  try {
@@ -8727,6 +9107,9 @@ program.command("init [name]").description("Create a new agent").option("-c, --c
8727
9107
  "--defaults",
8728
9108
  "Skip every prompt and use the same defaults the wizard would have suggested (description, default relays, no payments, no LLM, no encryption). Mutually exclusive with --config."
8729
9109
  ).option("--local", "Create in project <project>/.elisym/<name>/ (default: ~/.elisym/<name>/)").option(
9110
+ "--network <network>",
9111
+ "Solana network for the wallet entry: devnet (default) or mainnet (REAL funds, no faucet). The network is fixed at init - to go mainnet later, create a new agent."
9112
+ ).option(
8730
9113
  "--passphrase <value>",
8731
9114
  'Passphrase to encrypt secrets at rest. Empty string ("") skips encryption. Also reads ELISYM_PASSPHRASE env var. When neither is provided, prompts interactively.'
8732
9115
  ).option(
@@ -8756,16 +9139,16 @@ program.command("list").description("List all agents (project-local and home-glo
8756
9139
  }
8757
9140
  console.log("\nAgents:");
8758
9141
  for (const agent of agents) {
9142
+ const solana = await solanaLineFor(agent);
9143
+ const shadow = agent.shadowsGlobal ? " [shadows global]" : "";
8759
9144
  try {
8760
9145
  const loaded = await loadAgent(agent.name, cwd);
8761
9146
  const identity2 = ElisymIdentity.fromHex(loaded.secrets.nostr_secret_key);
8762
9147
  const npub = nip19.npubEncode(identity2.publicKey);
8763
- const solAddr = loaded.yaml.payments[0]?.address ? ` | Solana: ${loaded.yaml.payments[0].address}` : "";
8764
- const shadow = agent.shadowsGlobal ? " [shadows global]" : "";
8765
- console.log(` ${agent.name} (${agent.source})${shadow} | ${npub}${solAddr}`);
9148
+ console.log(` ${agent.name} (${agent.source})${shadow} | ${npub}${solana}`);
8766
9149
  } catch (e) {
8767
9150
  const hint = /encrypted secrets/i.test(e?.message ?? "") ? " (encrypted)" : "";
8768
- console.log(` ${agent.name} (${agent.source})${hint}`);
9151
+ console.log(` ${agent.name} (${agent.source})${shadow}${hint}${solana}`);
8769
9152
  }
8770
9153
  }
8771
9154
  console.log();