@elisym/cli 0.26.1 → 0.28.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,14 +2,14 @@
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';
10
10
  import { Command } from 'commander';
11
11
  import { mkdir, writeFile, readFile, mkdtemp, rm, rename, stat } from 'node:fs/promises';
12
- import { parseSkillMd, validateSkillFrontmatter, DEFAULT_X402_MAX_INPUT_BYTES, resolveInsidePathReal, DEFAULT_SCRIPT_TIMEOUT_MS, DynamicScriptSkill as DynamicScriptSkill$1, StaticScriptSkill as StaticScriptSkill$1, StaticFileSkill as StaticFileSkill$1, ScriptSkill as ScriptSkill$1 } from '@elisym/sdk/skills';
12
+ import { parseSkillMd, validateSkillFrontmatter, DEFAULT_X402_MAX_INPUT_BYTES, resolveInsidePathReal, DEFAULT_SCRIPT_TIMEOUT_MS, OnchainCallSkill, DynamicScriptSkill as DynamicScriptSkill$1, StaticScriptSkill as StaticScriptSkill$1, StaticFileSkill as StaticFileSkill$1, ScriptSkill as ScriptSkill$1 } from '@elisym/sdk/skills';
13
13
  import chalk from 'chalk';
14
14
  import Decimal from 'decimal.js-light';
15
15
  import { decodePaymentRequiredHeader } from '@x402/core/http';
@@ -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";
1513
+ }
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
+ }
1459
1522
  }
1460
- async function fetchUsdcBalance(rpc, owner) {
1461
- const mint = USDC_SOLANA_DEVNET.mint;
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)}`);
2200
+ console.log(` Network ${agentNetwork}`);
2201
+ console.log(` Quote ${formatAssetAmount(usdcAsset, quote)}`);
2082
2202
  console.log(
2083
- ` Your price ${formatAssetAmount(USDC_SOLANA_DEVNET, priceSubunits)} (margin ${marginPercent}% + protocol fee ${feePercent}%)`
2203
+ ` Your price ${formatAssetAmount(usdcAsset, priceSubunits)} (margin ${marginPercent}% + protocol fee ${feePercent}%)`
2084
2204
  );
2085
- console.log(` Net margin ${formatAssetAmount(USDC_SOLANA_DEVNET, netMargin)} per job`);
2086
- console.log(
2087
- ` Float ${formatAssetAmount(USDC_SOLANA_DEVNET, balance)} in ${signer.address}`
2088
- );
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);
@@ -6552,6 +6704,51 @@ var DynamicScriptSkill = class {
6552
6704
  return this.inner.execute(input, ctx);
6553
6705
  }
6554
6706
  };
6707
+ var OnchainSkill = class {
6708
+ name;
6709
+ description;
6710
+ capabilities;
6711
+ priceSubunits;
6712
+ asset;
6713
+ mode = "onchain";
6714
+ image;
6715
+ imageFile;
6716
+ dir;
6717
+ llmOverride;
6718
+ onchain;
6719
+ inner;
6720
+ constructor(params) {
6721
+ this.name = params.name;
6722
+ this.description = params.description;
6723
+ this.capabilities = params.capabilities;
6724
+ this.priceSubunits = params.priceSubunits;
6725
+ this.asset = params.asset;
6726
+ this.image = params.image;
6727
+ this.imageFile = params.imageFile;
6728
+ this.dir = params.dir;
6729
+ this.llmOverride = params.llmOverride;
6730
+ this.onchain = params.onchain;
6731
+ this.inner = new OnchainCallSkill({
6732
+ name: params.name,
6733
+ description: params.description,
6734
+ capabilities: params.capabilities,
6735
+ priceSubunits: BigInt(Math.round(params.priceSubunits)),
6736
+ asset: params.asset,
6737
+ scriptPath: params.scriptPath,
6738
+ scriptArgs: params.scriptArgs,
6739
+ scriptTimeoutMs: params.scriptTimeoutMs,
6740
+ scriptEnv: params.scriptEnv,
6741
+ image: params.image,
6742
+ imageFile: params.imageFile,
6743
+ llmOverride: params.llmOverride,
6744
+ onchain: params.onchain,
6745
+ network: params.network
6746
+ });
6747
+ }
6748
+ async execute(input, ctx) {
6749
+ return this.inner.execute(input, ctx);
6750
+ }
6751
+ };
6555
6752
  var ScriptSkill = class {
6556
6753
  name;
6557
6754
  description;
@@ -6684,7 +6881,7 @@ function scopeLlmKeys(scriptEnv, declaredProvider) {
6684
6881
  }
6685
6882
  return env;
6686
6883
  }
6687
- function buildCliSkill(parsed, entryPath, scriptEnv) {
6884
+ function buildCliSkill(parsed, entryPath, scriptEnv, network) {
6688
6885
  let safeImageFile = parsed.imageFile;
6689
6886
  if (safeImageFile !== void 0 && resolveInsidePathReal(entryPath, safeImageFile) === null) {
6690
6887
  console.warn(
@@ -6772,6 +6969,35 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
6772
6969
  }) : new StaticScriptSkill(scriptParams);
6773
6970
  break;
6774
6971
  }
6972
+ case "onchain": {
6973
+ if (parsed.script === void 0 || parsed.onchain === void 0) {
6974
+ throw new Error(
6975
+ `SKILL.md "${parsed.name}": internal error - script or onchain block missing for mode 'onchain'`
6976
+ );
6977
+ }
6978
+ const scriptPath = resolveInsidePathReal(entryPath, parsed.script);
6979
+ if (!scriptPath) {
6980
+ throw new Error(`SKILL.md "${parsed.name}": "script" must stay inside the skill directory`);
6981
+ }
6982
+ skill = new OnchainSkill({
6983
+ name: parsed.name,
6984
+ description: parsed.description,
6985
+ capabilities: parsed.capabilities,
6986
+ priceSubunits: Number(parsed.priceSubunits),
6987
+ asset: parsed.asset,
6988
+ scriptPath,
6989
+ scriptArgs: parsed.scriptArgs,
6990
+ scriptTimeoutMs: parsed.scriptTimeoutMs ?? DEFAULT_SCRIPT_TIMEOUT_MS,
6991
+ scriptEnv: scopeLlmKeys(scriptEnv, parsed.llmOverride?.provider),
6992
+ image: parsed.image,
6993
+ imageFile: safeImageFile,
6994
+ dir: entryPath,
6995
+ llmOverride: parsed.llmOverride,
6996
+ onchain: parsed.onchain,
6997
+ network
6998
+ });
6999
+ break;
7000
+ }
6775
7001
  case "x402": {
6776
7002
  if (parsed.x402 === void 0) {
6777
7003
  throw new Error(
@@ -6804,7 +7030,7 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
6804
7030
  }
6805
7031
  return skill;
6806
7032
  }
6807
- function loadSkillsFromDir(skillsDir, options = {}) {
7033
+ function loadSkillsFromDir(skillsDir, options) {
6808
7034
  const skills = [];
6809
7035
  let entries;
6810
7036
  try {
@@ -6826,12 +7052,18 @@ function loadSkillsFromDir(skillsDir, options = {}) {
6826
7052
  const content = readFileSync(skillMdPath, "utf-8");
6827
7053
  const { frontmatter, systemPrompt } = parseSkillMd(content);
6828
7054
  const parsed = validateSkillFrontmatter(frontmatter, systemPrompt, {
7055
+ network: options.network,
6829
7056
  allowFreeSkills: true,
6830
7057
  // The CLI runtime wires an x402 driver into the skill context, so
6831
7058
  // x402 skills are executable here (SDK-only hosts reject them).
6832
- allowX402Skills: true
7059
+ allowX402Skills: true,
7060
+ // Surface SDK load-time warnings (e.g. the devnet lsm->SOL pricing
7061
+ // fallback) in the start flow's ` !` console pattern.
7062
+ logger: {
7063
+ warn: (_obj, msg) => console.warn(` ! ${msg ?? ""}`)
7064
+ }
6833
7065
  });
6834
- skills.push(buildCliSkill(parsed, entryPath, options.scriptEnv));
7066
+ skills.push(buildCliSkill(parsed, entryPath, options.scriptEnv, options.network));
6835
7067
  } catch (e) {
6836
7068
  const message = e instanceof Error ? e.message : String(e);
6837
7069
  console.warn(` ! Skipping skill "${entry}": ${message}`);
@@ -7368,6 +7600,15 @@ function isTextContentType(contentType) {
7368
7600
  const mime = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
7369
7601
  return mime.startsWith("text/") || mime === "application/json" || mime.endsWith("+json");
7370
7602
  }
7603
+ function concatChunks(chunks, total) {
7604
+ const combined = new Uint8Array(total);
7605
+ let offset = 0;
7606
+ for (const chunk of chunks) {
7607
+ combined.set(chunk, offset);
7608
+ offset += chunk.byteLength;
7609
+ }
7610
+ return combined;
7611
+ }
7371
7612
  async function readBodyCapped(response, maxBytes) {
7372
7613
  const reader = response.body?.getReader();
7373
7614
  if (reader === void 0) {
@@ -7392,13 +7633,43 @@ async function readBodyCapped(response, maxBytes) {
7392
7633
  }
7393
7634
  chunks.push(value);
7394
7635
  }
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;
7636
+ return concatChunks(chunks, total);
7637
+ }
7638
+ async function readBodyPrefix(response, maxBytes, timeoutMs) {
7639
+ const reader = response.body?.getReader();
7640
+ if (reader === void 0) {
7641
+ return { text: "", truncated: false };
7400
7642
  }
7401
- return combined;
7643
+ const chunks = [];
7644
+ let total = 0;
7645
+ let ended = false;
7646
+ let timedOut = false;
7647
+ const deadline = setTimeout(() => {
7648
+ timedOut = true;
7649
+ reader.cancel().catch(() => {
7650
+ });
7651
+ }, timeoutMs);
7652
+ try {
7653
+ for (let reads = 0; reads < X402_MAX_ERROR_BODY_READS && total < maxBytes; reads += 1) {
7654
+ const { done, value } = await reader.read();
7655
+ if (done) {
7656
+ ended = true;
7657
+ break;
7658
+ }
7659
+ const fitting = value.subarray(0, maxBytes - total);
7660
+ total += fitting.byteLength;
7661
+ chunks.push(fitting);
7662
+ }
7663
+ } catch {
7664
+ } finally {
7665
+ clearTimeout(deadline);
7666
+ }
7667
+ reader.cancel().catch(() => {
7668
+ });
7669
+ return {
7670
+ text: new TextDecoder().decode(concatChunks(chunks, total)),
7671
+ truncated: !ended || timedOut
7672
+ };
7402
7673
  }
7403
7674
  async function capChallengeResponse(response) {
7404
7675
  if (response.status !== 402) {
@@ -7411,6 +7682,105 @@ async function capChallengeResponse(response) {
7411
7682
  headers: response.headers
7412
7683
  });
7413
7684
  }
7685
+ var PAYMENT_IDENTIFIER_EXTENSION = "payment-identifier";
7686
+ function asRecord(value) {
7687
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
7688
+ }
7689
+ function extensionInfo(declaration) {
7690
+ if (declaration === null) {
7691
+ return {};
7692
+ }
7693
+ return Object.hasOwn(declaration, "info") ? asRecord(declaration.info) ?? {} : declaration;
7694
+ }
7695
+ function requiredPaymentIdentifier(response) {
7696
+ const header = response.headers.get("payment-required");
7697
+ if (header === null || header.length > X402_MAX_CHALLENGE_HEADER_CHARS) {
7698
+ return null;
7699
+ }
7700
+ try {
7701
+ const decoded = JSON.parse(Buffer.from(header, "base64").toString("utf8"));
7702
+ const extensions = asRecord(asRecord(decoded)?.extensions);
7703
+ const declaration = asRecord(extensions?.[PAYMENT_IDENTIFIER_EXTENSION]);
7704
+ return extensionInfo(declaration).required === true ? declaration : null;
7705
+ } catch {
7706
+ return null;
7707
+ }
7708
+ }
7709
+ function withPaymentIdentifier(headerValue, declaration, jobId) {
7710
+ try {
7711
+ const record = asRecord(JSON.parse(Buffer.from(headerValue, "base64").toString("utf8")));
7712
+ if (record === null) {
7713
+ return headerValue;
7714
+ }
7715
+ const transaction = asRecord(record.payload)?.transaction;
7716
+ const signed = typeof transaction === "string" ? transaction : headerValue;
7717
+ const digest = createHash("sha256").update(`${jobId}:${signed}`).digest("hex");
7718
+ const id = `pay_${digest.slice(0, 32)}`;
7719
+ const extensions = asRecord(record.extensions) ?? {};
7720
+ const echoed = asRecord(extensions[PAYMENT_IDENTIFIER_EXTENSION]) ?? {};
7721
+ const info = { id, ...extensionInfo(declaration), ...extensionInfo(echoed) };
7722
+ record.extensions = {
7723
+ ...extensions,
7724
+ // Mirror the shape the server declared. Its echo VALIDATOR would accept
7725
+ // either (`@x402/core` normalizes both sides through `getExtensionInfo`
7726
+ // first), but whatever reads the id back out afterwards is the server's
7727
+ // own code, and the one place it can be counted on to look is where it
7728
+ // declared the extension.
7729
+ [PAYMENT_IDENTIFIER_EXTENSION]: Object.hasOwn(declaration, "info") ? { ...echoed, info } : info
7730
+ };
7731
+ return Buffer.from(JSON.stringify(record), "utf8").toString("base64");
7732
+ } catch {
7733
+ return headerValue;
7734
+ }
7735
+ }
7736
+ var UNICODE_FORMAT_MARKS = /\p{Cf}/gu;
7737
+ function flattenForOperator(text) {
7738
+ return sanitizeForTerminal(text).replace(UNICODE_FORMAT_MARKS, "").replace(/\s+/g, " ").trim();
7739
+ }
7740
+ function printedPrefix(flattened) {
7741
+ return flattened.length <= X402_ERROR_EXCERPT_CHARS ? flattened : flattened.slice(0, X402_ERROR_EXCERPT_CHARS).replace(/[\ud800-\udbff]$/, "");
7742
+ }
7743
+ function clipForOperator(flattened) {
7744
+ return flattened.length <= X402_ERROR_EXCERPT_CHARS ? flattened : `${printedPrefix(flattened)}...`;
7745
+ }
7746
+ function quoteUpstream(text) {
7747
+ return clipForOperator(flattenForOperator(text));
7748
+ }
7749
+ function quoteUpstreamWithInput(text, sentInput) {
7750
+ return clipForOperator(maskCustomerInput(flattenForOperator(text), inputForms(sentInput)));
7751
+ }
7752
+ function wireForm(sentInput) {
7753
+ return new TextDecoder().decode(new TextEncoder().encode(sentInput));
7754
+ }
7755
+ function jsonEscapedForm(sentInput) {
7756
+ return JSON.stringify(sentInput).slice(1, -1);
7757
+ }
7758
+ function urlEncodedForms(sentInput) {
7759
+ const forms = [new URLSearchParams({ q: sentInput }).toString().slice("q=".length)];
7760
+ try {
7761
+ forms.push(encodeURIComponent(sentInput));
7762
+ } catch {
7763
+ }
7764
+ return forms;
7765
+ }
7766
+ function endsInsideInput(text, forms) {
7767
+ const tail = text.replace(/\ufffd+$/, "").slice(-X402_MASKED_INPUT_MIN_CHARS);
7768
+ return tail.length >= X402_MASKED_INPUT_MIN_CHARS && forms.some((form) => form.includes(tail));
7769
+ }
7770
+ function inputForms(sentInput) {
7771
+ return [wireForm(sentInput), jsonEscapedForm(sentInput), ...urlEncodedForms(sentInput)].map(
7772
+ flattenForOperator
7773
+ );
7774
+ }
7775
+ function maskCustomerInput(text, forms) {
7776
+ let masked = text;
7777
+ for (const form of forms) {
7778
+ if (form.length >= X402_MASKED_INPUT_MIN_CHARS) {
7779
+ masked = masked.replaceAll(form, "[input]");
7780
+ }
7781
+ }
7782
+ return masked;
7783
+ }
7414
7784
  function abortableDelay(ms, signal) {
7415
7785
  return new Promise((resolve4) => {
7416
7786
  if (signal?.aborted) {
@@ -7433,15 +7803,17 @@ var X402Driver = class {
7433
7803
  this.options = options;
7434
7804
  this.store = new X402JobStore(options.agentDir);
7435
7805
  this.rpc = createSolanaRpc(options.rpcUrl);
7806
+ this.usdcAsset = resolveUsdcAsset(options.network);
7436
7807
  this.store.sweepExpired().catch(() => {
7437
7808
  });
7438
7809
  }
7439
7810
  store;
7440
7811
  rpc;
7812
+ usdcAsset;
7441
7813
  signerPromise = null;
7442
7814
  repriceHintLogged = /* @__PURE__ */ new Set();
7443
7815
  log(message) {
7444
- (this.options.log ?? console.log)(`[x402] ${message}`);
7816
+ (this.options.log ?? console.log)(`[x402] ${flattenForOperator(message)}`);
7445
7817
  }
7446
7818
  getSigner() {
7447
7819
  const secret = this.options.solanaSecretKeyBase58;
@@ -7498,13 +7870,16 @@ var X402Driver = class {
7498
7870
  return;
7499
7871
  }
7500
7872
  const signer = await this.assertWalletInvariant();
7501
- if (job.asset.mint !== USDC_SOLANA_DEVNET.mint || USDC_SOLANA_DEVNET.mint === void 0) {
7873
+ if (job.asset.mint !== this.usdcAsset.mint || this.usdcAsset.mint === void 0) {
7502
7874
  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\``
7875
+ `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
7876
  );
7505
7877
  }
7506
7878
  this.assertInputRules(job, input);
7507
- const rule = { maxUpstreamSubunits: job.params.maxUpstreamSubunits };
7879
+ const rule = {
7880
+ maxUpstreamSubunits: job.params.maxUpstreamSubunits,
7881
+ network: this.options.network
7882
+ };
7508
7883
  let probe;
7509
7884
  try {
7510
7885
  probe = await probePaymentRequiredCached(
@@ -7514,10 +7889,10 @@ var X402Driver = class {
7514
7889
  );
7515
7890
  } catch (error) {
7516
7891
  if (error instanceof X402ProbeError) {
7517
- throw new X402PreflightError(`upstream probe failed: ${error.message}`);
7892
+ throw new X402PreflightError(`upstream probe failed: ${quoteUpstream(error.message)}`);
7518
7893
  }
7519
7894
  const message = error instanceof Error ? error.message : String(error);
7520
- throw new X402PreflightError(`upstream unreachable: ${message}`);
7895
+ throw new X402PreflightError(`upstream unreachable: ${quoteUpstream(message)}`);
7521
7896
  }
7522
7897
  const quote = maxAcceptableQuote(probe.accepts, rule);
7523
7898
  if (quote === null) {
@@ -7528,13 +7903,18 @@ var X402Driver = class {
7528
7903
  if (quote < job.params.maxUpstreamSubunits && !this.repriceHintLogged.has(job.skillName)) {
7529
7904
  this.repriceHintLogged.add(job.skillName);
7530
7905
  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`
7906
+ `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`
7907
+ );
7908
+ }
7909
+ const balance = await fetchUsdcBalance(this.rpc, address(signer.address), this.options.network);
7910
+ if (balance === null) {
7911
+ throw new X402PreflightError(
7912
+ `could not read the bridge float balance for ${signer.address} - refusing to pay the upstream without verifying it; retry in a moment`
7532
7913
  );
7533
7914
  }
7534
- const balance = await fetchUsdcBalance(this.rpc, address(signer.address));
7535
7915
  if (balance < quote) {
7536
7916
  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`
7917
+ `bridge float insufficient: ${formatAssetAmount(this.usdcAsset, balance)} USDC in ${signer.address}, upstream quotes ${formatAssetAmount(this.usdcAsset, quote)} - top up the wallet`
7538
7918
  );
7539
7919
  }
7540
7920
  const feeBps = await this.options.getFeeBps();
@@ -7542,7 +7922,7 @@ var X402Driver = class {
7542
7922
  const net = BigInt(job.priceSubunits - fee);
7543
7923
  if (net < quote) {
7544
7924
  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`
7925
+ `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
7926
  );
7547
7927
  }
7548
7928
  }
@@ -7558,6 +7938,7 @@ var X402Driver = class {
7558
7938
  */
7559
7939
  buildInstrumentedFetch(jobId, attemptState) {
7560
7940
  const store = this.store;
7941
+ let requiredIdentifier = null;
7561
7942
  return async function instrumentedFetch(info, init) {
7562
7943
  const request = new Request(info, init);
7563
7944
  if (request.headers.has("PAYMENT-SIGNATURE") || request.headers.has("X-PAYMENT")) {
@@ -7572,19 +7953,30 @@ var X402Driver = class {
7572
7953
  );
7573
7954
  }
7574
7955
  attemptState.paymentSent = true;
7956
+ const signature = request.headers.get("PAYMENT-SIGNATURE");
7957
+ if (requiredIdentifier !== null && signature !== null) {
7958
+ request.headers.set(
7959
+ "PAYMENT-SIGNATURE",
7960
+ withPaymentIdentifier(signature, requiredIdentifier, jobId)
7961
+ );
7962
+ }
7575
7963
  }
7576
7964
  const response = await globalThis.fetch(new Request(request, { redirect: "error" }));
7965
+ if (response.status === 402) {
7966
+ requiredIdentifier = requiredPaymentIdentifier(response);
7967
+ }
7577
7968
  return capChallengeResponse(response);
7578
7969
  };
7579
7970
  }
7580
7971
  buildClient(signer, rule) {
7581
7972
  const scheme = new ExactSvmScheme(signer, { rpcUrl: this.options.rpcUrl });
7973
+ const networkIds = x402SolanaNetworkIds(this.options.network);
7582
7974
  return x402Client.fromConfig({
7583
- schemes: [{ network: X402_SOLANA_DEVNET_CAIP2, client: scheme }],
7975
+ schemes: [{ network: networkIds.caip2, client: scheme }],
7584
7976
  policies: [buildRequirementsPolicy(rule)]
7585
- }).registerV1(X402_SOLANA_DEVNET_V1, scheme);
7977
+ }).registerV1(networkIds.v1, scheme);
7586
7978
  }
7587
- classifyWrapperError(error) {
7979
+ classifyWrapperError(error, sentInput) {
7588
7980
  if (error instanceof Error && error.name === "AbortError") {
7589
7981
  return error;
7590
7982
  }
@@ -7592,21 +7984,63 @@ var X402Driver = class {
7592
7984
  return error;
7593
7985
  }
7594
7986
  const message = error instanceof Error ? error.message : String(error);
7987
+ const quoted = quoteUpstreamWithInput(message, sentInput);
7595
7988
  if (message.includes("Failed to create payment payload")) {
7596
7989
  return new X402PermanentError(
7597
- `payment refused by policy (upstream likely repriced above x402_max_upstream): ${message}`
7990
+ `payment refused by policy (upstream likely repriced above x402_max_upstream): ${quoted}`
7991
+ );
7992
+ }
7993
+ return new X402TransientError(`upstream request failed: ${quoted}`);
7994
+ }
7995
+ /**
7996
+ * Read a short, safe excerpt of a failing upstream's body.
7997
+ *
7998
+ * A bare status code is not a diagnosis: a 4xx from an x402 upstream can
7999
+ * mean a rejected payload, a missing required extension, or an argument the
8000
+ * service did not like, and the operator cannot tell which without the
8001
+ * body's own words. The body is untrusted, so only a bounded prefix is read
8002
+ * at all, and it is stripped of anything that could forge a log line before
8003
+ * it reaches the operator's. Reading it never fails the caller - an
8004
+ * unreadable body simply yields no excerpt, which is exactly the
8005
+ * status-only message as before.
8006
+ *
8007
+ * `sentInput` is the customer's own job input, masked out of the quote by
8008
+ * `maskCustomerInput`: an upstream that echoes the request back would
8009
+ * otherwise put it in the operator's log, which the runtime keeps it out of
8010
+ * everywhere else.
8011
+ */
8012
+ async errorExcerpt(response, sentInput) {
8013
+ try {
8014
+ const body = await readBodyPrefix(
8015
+ response,
8016
+ X402_MAX_ERROR_BODY_BYTES,
8017
+ this.options.errorBodyReadMs ?? X402_ERROR_BODY_READ_MS
7598
8018
  );
8019
+ const forms = inputForms(sentInput);
8020
+ const quoted = maskCustomerInput(flattenForOperator(body.text), forms);
8021
+ if (quoted.length === 0) {
8022
+ return "";
8023
+ }
8024
+ const shown = printedPrefix(quoted);
8025
+ if (body.truncated && endsInsideInput(shown, forms)) {
8026
+ return "";
8027
+ }
8028
+ return `: ${clipForOperator(quoted)}`;
8029
+ } catch {
8030
+ return "";
7599
8031
  }
7600
- return new X402TransientError(`upstream request failed: ${message}`);
7601
8032
  }
7602
- classifyStatus(status) {
8033
+ classifyStatus(status, excerpt) {
7603
8034
  if (status === 402) {
7604
- return new X402TransientError("upstream still returned 402 after a payment attempt", status);
8035
+ return new X402TransientError(
8036
+ `upstream still returned 402 after a payment attempt${excerpt}`,
8037
+ status
8038
+ );
7605
8039
  }
7606
8040
  if (status === 429 || status >= 500) {
7607
- return new X402TransientError(`upstream returned ${status}`, status);
8041
+ return new X402TransientError(`upstream returned ${status}${excerpt}`, status);
7608
8042
  }
7609
- return new X402PermanentError(`upstream returned ${status}`);
8043
+ return new X402PermanentError(`upstream returned ${status}${excerpt}`);
7610
8044
  }
7611
8045
  async execute(job, input, signal) {
7612
8046
  const cached = await this.store.getResult(input.jobId);
@@ -7637,7 +8071,10 @@ var X402Driver = class {
7637
8071
  headers["content-type"] = jsonContentType(input.data) ? "application/json" : "text/plain";
7638
8072
  }
7639
8073
  const signer = await this.assertWalletInvariant();
7640
- const rule = { maxUpstreamSubunits: job.params.maxUpstreamSubunits };
8074
+ const rule = {
8075
+ maxUpstreamSubunits: job.params.maxUpstreamSubunits,
8076
+ network: this.options.network
8077
+ };
7641
8078
  const client = this.buildClient(signer, rule);
7642
8079
  const jobTag = input.jobId.slice(0, 8);
7643
8080
  let freeRetriesUsed = 0;
@@ -7650,7 +8087,7 @@ var X402Driver = class {
7650
8087
  requestUrl,
7651
8088
  headers,
7652
8089
  body,
7653
- input.jobId,
8090
+ input,
7654
8091
  client,
7655
8092
  attemptState,
7656
8093
  signal
@@ -7698,9 +8135,9 @@ var X402Driver = class {
7698
8135
  }
7699
8136
  }
7700
8137
  /** One full payment-wrapper pass against the upstream; throws classified errors. */
7701
- async attemptUpstream(job, requestUrl, headers, body, jobId, client, attemptState, signal) {
8138
+ async attemptUpstream(job, requestUrl, headers, body, input, client, attemptState, signal) {
7702
8139
  const fetchWithPayment = wrapFetchWithPayment(
7703
- this.buildInstrumentedFetch(jobId, attemptState),
8140
+ this.buildInstrumentedFetch(input.jobId, attemptState),
7704
8141
  client
7705
8142
  );
7706
8143
  let response;
@@ -7712,27 +8149,25 @@ var X402Driver = class {
7712
8149
  signal
7713
8150
  });
7714
8151
  } catch (error) {
7715
- throw this.classifyWrapperError(error);
8152
+ throw this.classifyWrapperError(error, input.data);
7716
8153
  }
7717
8154
  if (!response.ok) {
7718
- await response.body?.cancel().catch(() => {
7719
- });
7720
- throw this.classifyStatus(response.status);
8155
+ throw this.classifyStatus(response.status, await this.errorExcerpt(response, input.data));
7721
8156
  }
7722
8157
  let bytes;
7723
8158
  try {
7724
8159
  bytes = await readBodyCapped(response, X402_MAX_RESPONSE_BYTES);
7725
8160
  } catch (error) {
7726
- throw this.classifyWrapperError(error);
8161
+ throw this.classifyWrapperError(error, input.data);
7727
8162
  }
7728
8163
  const contentType = response.headers.get("content-type") ?? "application/octet-stream";
7729
8164
  if (isTextContentType(contentType)) {
7730
8165
  const data = new TextDecoder().decode(bytes);
7731
- await this.store.saveTextResult(jobId, data);
8166
+ await this.store.saveTextResult(input.jobId, data);
7732
8167
  return { data };
7733
8168
  }
7734
8169
  const mime = contentType.split(";")[0]?.trim() ?? "application/octet-stream";
7735
- const filePath = await this.store.saveFileResult(jobId, mime, bytes);
8170
+ const filePath = await this.store.saveFileResult(input.jobId, mime, bytes);
7736
8171
  return { data: "", outputMime: mime, filePath };
7737
8172
  }
7738
8173
  };
@@ -7770,9 +8205,10 @@ async function cmdStart(nameArg, options = {}) {
7770
8205
  const rpcUrl = getRpcUrl(walletNetwork);
7771
8206
  const rpc = createSolanaRpc(rpcUrl);
7772
8207
  const walletAddress = address(solanaAddress);
7773
- const [{ value: balanceLamports }, usdcBalance] = await Promise.all([
8208
+ const splAssets = splAssetsForNetwork(walletNetwork);
8209
+ const [{ value: balanceLamports }, ...splBalances] = await Promise.all([
7774
8210
  rpc.getBalance(walletAddress).send(),
7775
- fetchUsdcBalance(rpc, walletAddress)
8211
+ ...splAssets.map((asset) => fetchSplBalance(rpc, walletAddress, asset))
7776
8212
  ]);
7777
8213
  const balance = Number(balanceLamports);
7778
8214
  console.log(" Wallet");
@@ -7782,9 +8218,16 @@ async function cmdStart(nameArg, options = {}) {
7782
8218
  console.log(` RPC ${stripRpcSecrets(process.env.SOLANA_RPC_URL)} (custom)`);
7783
8219
  }
7784
8220
  console.log(` SOL ${formatSol(balance)} (${balance} lamports)`);
7785
- console.log(` USDC ${formatAssetAmount(USDC_SOLANA_DEVNET, usdcBalance)}`);
8221
+ for (const [index, asset] of splAssets.entries()) {
8222
+ const symbolColumn = asset.symbol.padEnd(8);
8223
+ console.log(` ${symbolColumn} ${formatSplBalanceValue(asset, splBalances[index])}`);
8224
+ }
7786
8225
  if (balance === 0) {
7787
- console.log(" ! Wallet is empty. Get devnet SOL: https://faucet.solana.com");
8226
+ if (walletNetwork === "mainnet") {
8227
+ console.log(" ! Wallet is empty. Fund it with real SOL to operate on mainnet.");
8228
+ } else {
8229
+ console.log(" ! Wallet is empty. Get devnet SOL: https://faucet.solana.com");
8230
+ }
7788
8231
  }
7789
8232
  console.log();
7790
8233
  } catch (e) {
@@ -7793,10 +8236,15 @@ async function cmdStart(nameArg, options = {}) {
7793
8236
  `);
7794
8237
  }
7795
8238
  }
8239
+ if (walletNetwork === "mainnet") {
8240
+ console.log(
8241
+ " ! Mainnet: per-skill prices are REAL funds. Review each SKILL.md price - skills copied from a devnet agent keep their absolute amounts.\n"
8242
+ );
8243
+ }
7796
8244
  const scriptEnv = buildScriptEnv(loaded.secrets);
7797
8245
  const paths = agentPaths(loaded.dir);
7798
8246
  const skillsDir = paths.skills;
7799
- const allSkills = loadSkillsFromDir(skillsDir, { scriptEnv });
8247
+ const allSkills = loadSkillsFromDir(skillsDir, { network: walletNetwork, scriptEnv });
7800
8248
  if (allSkills.length === 0) {
7801
8249
  console.error(` ! No skills found in ${skillsDir}
7802
8250
  `);
@@ -7994,6 +8442,12 @@ async function cmdStart(nameArg, options = {}) {
7994
8442
  agentName,
7995
8443
  agentDescription: loaded.yaml.description ?? ""
7996
8444
  };
8445
+ const onchainSkillCount = allSkills.filter((skill) => skill.onchain !== void 0).length;
8446
+ if (solanaAddress === void 0 && onchainSkillCount > 0) {
8447
+ console.warn(
8448
+ ` ! ${onchainSkillCount} onchain capability(ies) declare a block but this agent has no Solana payment address, so the network their cards would promise is a guess. Add a payments entry to elisym.yaml.`
8449
+ );
8450
+ }
7997
8451
  const x402Skills = allSkills.filter((skill) => skill.mode === "x402");
7998
8452
  let x402InvariantBroken;
7999
8453
  if (x402Skills.length > 0) {
@@ -8003,15 +8457,19 @@ async function cmdStart(nameArg, options = {}) {
8003
8457
  } else if (!solanaAddress) {
8004
8458
  x402InvariantBroken = "no payments[] entry in elisym.yaml";
8005
8459
  } else {
8006
- const bridgeSigner = await signerFromSecretKeyBase58(solanaSecretKey);
8007
- if (bridgeSigner.address !== solanaAddress) {
8008
- x402InvariantBroken = `payments[].address (${solanaAddress}) differs from the solana_secret_key address (${bridgeSigner.address})`;
8460
+ try {
8461
+ const bridgeSigner = await signerFromSecretKeyBase58(solanaSecretKey);
8462
+ if (bridgeSigner.address !== solanaAddress) {
8463
+ x402InvariantBroken = `payments[].address (${solanaAddress}) differs from the solana_secret_key address (${bridgeSigner.address})`;
8464
+ }
8465
+ } catch (error) {
8466
+ x402InvariantBroken = `solana_secret_key is present but unreadable - ${error instanceof Error ? error.message : String(error)}`;
8009
8467
  }
8010
8468
  }
8011
8469
  for (const skill of x402Skills) {
8012
- if (skill.asset.mint !== USDC_SOLANA_DEVNET.mint) {
8470
+ if (skill.asset.mint !== resolveUsdcAsset(walletNetwork).mint) {
8013
8471
  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\`.`
8472
+ ` ! 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
8473
  );
8016
8474
  }
8017
8475
  }
@@ -8021,24 +8479,27 @@ async function cmdStart(nameArg, options = {}) {
8021
8479
  " ! x402 skills will NOT be advertised and their jobs will be refused before payment."
8022
8480
  );
8023
8481
  console.warn(" ! Fix: re-run `npx @elisym/cli x402 add` or align elisym.yaml with the key.");
8482
+ } else {
8483
+ const x402RpcUrl = getRpcUrl(walletNetwork);
8484
+ async function fetchLiveFeeBps() {
8485
+ const config = await getProtocolConfig(
8486
+ createSolanaRpc(x402RpcUrl),
8487
+ getProtocolProgramId(walletNetwork),
8488
+ walletNetwork,
8489
+ { forceRefresh: true }
8490
+ );
8491
+ return config.feeBps;
8492
+ }
8493
+ skillCtx.x402Driver = new X402Driver({
8494
+ agentDir: loaded.dir,
8495
+ paymentsAddress: solanaAddress,
8496
+ solanaSecretKeyBase58: loaded.secrets.solana_secret_key,
8497
+ rpcUrl: x402RpcUrl,
8498
+ network: walletNetwork,
8499
+ getFeeBps: fetchLiveFeeBps,
8500
+ log: (message) => console.log(` ${message}`)
8501
+ });
8024
8502
  }
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
8503
  }
8043
8504
  console.log(" Connecting to relays and publishing capabilities...");
8044
8505
  const identity2 = ElisymIdentity.fromHex(loaded.secrets.nostr_secret_key);
@@ -8216,11 +8677,7 @@ async function cmdStart(nameArg, options = {}) {
8216
8677
  const declaresDelegation = allSkills.some((skill) => skill.delegation !== void 0);
8217
8678
  let delegatePubkey;
8218
8679
  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) {
8680
+ if (declaresDelegation) {
8224
8681
  const delegateSecret = loaded.secrets.solana_delegate_secret_key;
8225
8682
  if (delegateSecret && delegateSecret.length > 0) {
8226
8683
  try {
@@ -8231,7 +8688,7 @@ async function cmdStart(nameArg, options = {}) {
8231
8688
  const { value: delegateLamports } = await createSolanaRpc(getRpcUrl(walletNetwork)).getBalance(address(delegatePubkey)).send();
8232
8689
  if (delegateLamports === 0n) {
8233
8690
  console.warn(
8234
- " ! Delegate key holds no SOL - delegated job pulls will fail on tx fees. Fund it: https://faucet.solana.com"
8691
+ " ! 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
8692
  );
8236
8693
  }
8237
8694
  } catch {
@@ -8253,38 +8710,7 @@ async function cmdStart(nameArg, options = {}) {
8253
8710
  console.warn(` ! Run \`npx @elisym/cli delegate-key ${loaded.name}\` to enable it.`);
8254
8711
  }
8255
8712
  }
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
- }
8713
+ const buildCard = (skill) => buildCapabilityCard(skill, { walletNetwork, solanaAddress, delegatePubkey });
8288
8714
  function retiredModelKeyForSkill(skill) {
8289
8715
  if (skill.resolvedTriple) {
8290
8716
  return `${skill.resolvedTriple.provider}::${skill.resolvedTriple.model}`;
@@ -8313,6 +8739,16 @@ async function cmdStart(nameArg, options = {}) {
8313
8739
  );
8314
8740
  continue;
8315
8741
  }
8742
+ if (withholdsOnchainCard(skill, solanaAddress)) {
8743
+ console.warn(
8744
+ ` ! Not republishing "${skill.name}" - no Solana payments entry. An earlier card for it, if any, stays on the relays until it expires.`
8745
+ );
8746
+ logger.warn(
8747
+ { event: "publish_skipped_onchain_no_wallet", kind: 31990, skill: skill.name },
8748
+ "capability not advertised - onchain block with no Solana payment address"
8749
+ );
8750
+ continue;
8751
+ }
8316
8752
  try {
8317
8753
  await client.discovery.publishCapability(identity2, buildCard(skill), kinds);
8318
8754
  cardsPublished += 1;
@@ -8391,7 +8827,7 @@ async function cmdStart(nameArg, options = {}) {
8391
8827
  if (nonceStore !== void 0) {
8392
8828
  await ensureGitignoreHasDelegationNoncesEntry(dirname(loaded.dir));
8393
8829
  }
8394
- const rpcUrlForLog = stripRpcSecrets(process.env.SOLANA_RPC_URL ?? getRpcUrl());
8830
+ const rpcUrlForLog = stripRpcSecrets(process.env.SOLANA_RPC_URL ?? getRpcUrl(walletNetwork));
8395
8831
  logger.debug(
8396
8832
  {
8397
8833
  event: "config_resolved",
@@ -8469,6 +8905,49 @@ async function cmdStart(nameArg, options = {}) {
8469
8905
  console.log(" * Running. Press Ctrl+C to stop.\n");
8470
8906
  await runtime.run();
8471
8907
  }
8908
+ function buildCapabilityCard(skill, inputs) {
8909
+ const { walletNetwork, solanaAddress, delegatePubkey } = inputs;
8910
+ const isStatic = skill.mode === "static-file" || skill.mode === "static-script" || skill.noInput === true;
8911
+ return {
8912
+ name: skill.name,
8913
+ description: skill.description,
8914
+ capabilities: skill.capabilities,
8915
+ image: skill.image,
8916
+ ...isStatic ? { static: true } : {},
8917
+ // File-exchange hints (dynamic-script only). `inputMime` flags a file input;
8918
+ // `inputText` tells the web whether to also show its text box for that file job.
8919
+ ...skill.inputMime ? { inputMime: skill.inputMime } : {},
8920
+ ...skill.inputText ? { inputText: skill.inputText } : {},
8921
+ ...skill.outputMime ? { outputMime: skill.outputMime } : {},
8922
+ // Conversation-context flag: clients gate chat affordances (session-carrying
8923
+ // sends) on it. Strict true only - the read side coerces anything else away.
8924
+ ...skill.context === true ? { context: true } : {},
8925
+ // Delegation descriptor: only when the skill opts in AND a delegate key
8926
+ // exists (delegatePubkey resolved). The pubkey is injected here, never
8927
+ // taken from the SKILL.md - the operator declares only the cap/mechanism.
8928
+ ...skill.delegation && delegatePubkey ? { delegation: { ...skill.delegation, delegate_pubkey: delegatePubkey } } : {},
8929
+ // On-chain promise (`mode: onchain`): the network is stamped from the
8930
+ // agent's wallet here, never from SKILL.md, so a capability copied between
8931
+ // a devnet and a mainnet agent cannot advertise the wrong chain.
8932
+ // Only stamped when the agent actually has a Solana payment entry: without
8933
+ // one `walletNetwork` is a default, and a card must never promise a chain
8934
+ // the operator never chose. `cmdStart` warns for such a skill at step 7a.
8935
+ ...skill.onchain && solanaAddress ? { onchain: { ...skill.onchain, network: walletNetwork } } : {},
8936
+ payment: solanaAddress ? {
8937
+ chain: "solana",
8938
+ network: walletNetwork,
8939
+ address: solanaAddress,
8940
+ job_price: skill.priceSubunits,
8941
+ token: skill.asset.token,
8942
+ ...skill.asset.mint ? { mint: skill.asset.mint } : {},
8943
+ decimals: skill.asset.decimals,
8944
+ symbol: skill.asset.symbol
8945
+ } : void 0
8946
+ };
8947
+ }
8948
+ function withholdsOnchainCard(skill, solanaAddress) {
8949
+ return skill.onchain !== void 0 && solanaAddress === void 0;
8950
+ }
8472
8951
  function buildScriptEnv(secrets) {
8473
8952
  const scriptEnv = { ...process.env };
8474
8953
  delete scriptEnv.ELISYM_PASSPHRASE;
@@ -8689,15 +9168,20 @@ async function cmdWallet(name) {
8689
9168
  const rpcUrl = getRpcUrl(solPayment.network);
8690
9169
  const rpc = createSolanaRpc(rpcUrl);
8691
9170
  const walletAddress = address(solPayment.address);
8692
- const { value: balance } = await rpc.getBalance(walletAddress).send();
8693
- const usdcBalance = await fetchUsdcBalance(rpc, walletAddress);
9171
+ const splAssets = splAssetsForNetwork(solPayment.network);
9172
+ const [{ value: balance }, ...splBalances] = await Promise.all([
9173
+ rpc.getBalance(walletAddress).send(),
9174
+ ...splAssets.map((asset) => fetchSplBalance(rpc, walletAddress, asset))
9175
+ ]);
8694
9176
  console.log(`
8695
9177
  Agent: ${name}`);
8696
9178
  console.log(` Network: ${solPayment.network}`);
8697
9179
  console.log(` Address: ${solPayment.address}`);
8698
9180
  console.log(` SOL balance: ${formatSol(Number(balance))} (${balance} lamports)`);
8699
- console.log(` USDC balance: ${formatAssetAmount(USDC_SOLANA_DEVNET, usdcBalance)}
8700
- `);
9181
+ for (const [index, asset] of splAssets.entries()) {
9182
+ console.log(` ${asset.symbol} balance: ${formatSplBalanceValue(asset, splBalances[index])}`);
9183
+ }
9184
+ console.log();
8701
9185
  }
8702
9186
  function readPackageVersion() {
8703
9187
  try {
@@ -8727,6 +9211,9 @@ program.command("init [name]").description("Create a new agent").option("-c, --c
8727
9211
  "--defaults",
8728
9212
  "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
9213
  ).option("--local", "Create in project <project>/.elisym/<name>/ (default: ~/.elisym/<name>/)").option(
9214
+ "--network <network>",
9215
+ "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."
9216
+ ).option(
8730
9217
  "--passphrase <value>",
8731
9218
  'Passphrase to encrypt secrets at rest. Empty string ("") skips encryption. Also reads ELISYM_PASSPHRASE env var. When neither is provided, prompts interactively.'
8732
9219
  ).option(
@@ -8756,16 +9243,16 @@ program.command("list").description("List all agents (project-local and home-glo
8756
9243
  }
8757
9244
  console.log("\nAgents:");
8758
9245
  for (const agent of agents) {
9246
+ const solana = await solanaLineFor(agent);
9247
+ const shadow = agent.shadowsGlobal ? " [shadows global]" : "";
8759
9248
  try {
8760
9249
  const loaded = await loadAgent(agent.name, cwd);
8761
9250
  const identity2 = ElisymIdentity.fromHex(loaded.secrets.nostr_secret_key);
8762
9251
  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}`);
9252
+ console.log(` ${agent.name} (${agent.source})${shadow} | ${npub}${solana}`);
8766
9253
  } catch (e) {
8767
9254
  const hint = /encrypted secrets/i.test(e?.message ?? "") ? " (encrypted)" : "";
8768
- console.log(` ${agent.name} (${agent.source})${hint}`);
9255
+ console.log(` ${agent.name} (${agent.source})${shadow}${hint}${solana}`);
8769
9256
  }
8770
9257
  }
8771
9258
  console.log();