@candledottv/cli 0.11.39 → 0.11.40

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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +333 -69
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -122,7 +122,7 @@ emergency procedure, rebinding, trusted wallets and API key access levels are al
122
122
  | `candle swap <from> <to> --amount <n>\|--percent <n> --wallet <tee> [--to <tee>] [--wait]` | Quote, confirm and swap through the TEE wallet's bound key; the first buy after a launch is this command, unless `candle launch --buy` made it. The assets decide the chain (ETH, USDG or a 0x token is Hood; SOL, USDC, CNDL or a mint is Solana). SOL or USDC to ETH or USDG, or back, is a bridge through Relay into this key's own TEE wallet on the other chain (`--to` names it when there are several); `--wait` follows it for up to ten minutes. |
123
123
  | `candle swap status <id> [--kind trade\|swap\|launch] [--wait]` | Read an operation without resending it; `--wait` follows a bridge until it ends. |
124
124
  | `candle transfer --to <address\|wallet name\|vault> --asset <SOL\|USDC\|CNDL\|ETH\|USDG>\|--mint <mint>\|--token <0x...> --amount <decimal\|max> [--wallet <tee>] [--rpc-url <url>] [--yes]` | Move funds out of a TEE wallet through its bound Withdraw key, to the wallet's own pinned vault (`--to vault`, any token, `max` allowed, one SOL transfer at most the server's backstop ceiling, 50 SOL by default) or to another of the account's wallets you linked while signed in or marked trusted (a base asset, counted against the key's caps, which must include a per-asset cap and a USD `--tx-limit`). The asset decides the chain: ETH, USDG or `--token` moves from a Hood TEE wallet. The destination and its kind are shown before you confirm; Candle builds, the relay signs, Candle broadcasts. |
125
- | `candle launch --name <name> --symbol <symbol> --image <path>\|--image-url <url> --wallet <tee> [--banner <file\|URL>] [--buy <amount>] [--quote-asset <asset>] [--dex-version v3\|v4] [--mode <mode>] [--curve-v <V> --curve-g <G>] [--creator-fee-bps 0\|100\|200] [--pool-fee 25\|50\|100] [--pool-open legacy\|curve_price] [--exp-pool-shape floor\|full_range] [--dry-run]` | Create a token from a TEE wallet. The image is `--image-url` (an `https://` URL) or `--image <path>` (a PNG, JPEG, GIF or WebP file on this machine, at most 10MB), never both; `--image` uploads the file before the confirm prompt, which makes it public and permanent, and launches with the URL that comes back. `--banner <file|URL>` adds a wide banner from a local file or an https URL. Local banners use `kind=banner`, retain their original shape (wider than 1.5:1), and become public and permanent before confirmation; the result reports `bannerUrl`. Without `--buy` it makes no first buy. `--buy <amount>` adds one from the same wallet, in whole units of the quote asset, reserved against the key's spend window before signing: in the launch transaction on Solana, as a last leg on Hood. It needs the server's `TEE_LAUNCH_BUYS_ENABLED`, which is off by default; while it is off Candle refuses the buy and nothing is built. Needs `launch:write`, `allowLaunch` turned on for the wallet by the account owner, and the Pro or Max plan. On Free, launch from the embedded wallet instead, with an optional dev buy (same transaction on Solana; best-effort follow-up on Hood): the MCP tool `candle_launch_and_seed` or the SDK's `launch()`. The wallet decides the chain: a Hood TEE wallet launches on Hood and needs `--dex-version`. `--quote-asset` is sol, usdc or cndl on Solana, eth or usdg on Hood. `--mode` picks the curve: `open` or `exclusive`, `test-open` or `test-exclusive` where the server has `ENABLE_TEST_CURVES` on, or on Solana with sol or cndl `exp-a`, `exp-b`, `exp-c` and `exp-custom`, which takes `--curve-v <V>` and `--curve-g <G>` in whole units of the quote asset where the server has `EXP_CUSTOM_CURVES` on. `--creator-fee-bps 100` or `200` adds an extra creator fee on an `exp-` mode. On an `exp-` curve the first buy is at most 0.5 SOL (2,000,000 CNDL), or 10% of the threshold when that is lower. Solana pool choices: `--pool-fee 25|50|100` (bps; add `--pool-fee-from <bps> --pool-fee-minutes <min>` for an anti-snipe fall to that rate, at most 2500 bps over 30 minutes), `--pool-open curve_price` (preset tiers) and `--exp-pool-shape full_range` (exp- curves); omitted, the pool is today's. `--dry-run` prints Candle's preview of the launch (the curve's terms, the first buy's share, the fees, how the pool opens at graduation, the dev-buy ceiling) and builds, signs and records nothing; with `--image` the upload still happens. |
125
+ | `candle launch --name <name> --symbol <symbol> --image <path>\|--image-url <url> --wallet <tee>\|embedded [--banner <file\|URL>] [--buy <amount>] [--quote-asset <asset>] [--dex-version v3\|v4] [--mode <mode>] [--curve-v <V> --curve-g <G>] [--creator-fee-bps 0\|100\|200] [--pool-fee 25\|50\|100] [--pool-open legacy\|curve_price] [--exp-pool-shape floor\|full_range] [--dry-run]` | Create a token from a TEE wallet. The image is `--image-url` (an `https://` URL) or `--image <path>` (a PNG, JPEG, GIF or WebP file on this machine, at most 10MB), never both; `--image` uploads the file before the confirm prompt, which makes it public and permanent, and launches with the URL that comes back. `--banner <file|URL>` adds a wide banner from a local file or an https URL. Local banners use `kind=banner`, retain their original shape (wider than 1.5:1), and become public and permanent before confirmation; the result reports `bannerUrl`. Without `--buy` it makes no first buy. `--buy <amount>` adds one from the same wallet, in whole units of the quote asset, reserved against the key's spend window before signing: as its own leg in one Jito bundle with the launch on Solana, as a last leg on Hood. It needs the server's `TEE_LAUNCH_BUYS_ENABLED`, which is off by default; while it is off Candle refuses the buy and nothing is built. Needs `launch:write`, `allowLaunch` turned on for the wallet by the account owner, and the Pro or Max plan. `--wallet embedded --buy <amount>` launches from the account's embedded wallet instead, on Solana, as an atomic launch: Candle's dry run first (nothing built or recorded), the quote to confirm, then one build and one `/launch/atomic/submit`, with Candle signing both legs into one Jito bundle. It needs Pro or Max, `launch:write` and `swap:write`, and does not depend on `TEE_LAUNCH_BUYS_ENABLED`. Without a first buy, every plan can launch from the embedded wallet with the MCP tool `candle_launch_token` or the SDK's `launch()`. The wallet decides the chain: a Hood TEE wallet launches on Hood and needs `--dex-version`. `--quote-asset` is sol, usdc or cndl on Solana, eth or usdg on Hood. `--mode` picks the curve: `open` or `exclusive`, `test-open` or `test-exclusive` where the server has `ENABLE_TEST_CURVES` on, or on Solana with sol or cndl `exp-a`, `exp-b`, `exp-c` and `exp-custom`, which takes `--curve-v <V>` and `--curve-g <G>` in whole units of the quote asset where the server has `EXP_CUSTOM_CURVES` on. `--creator-fee-bps 100` or `200` adds an extra creator fee on an `exp-` mode. On an `exp-` curve the first buy is at most 0.5 SOL (2,000,000 CNDL), or 10% of the threshold when that is lower. Solana pool choices: `--pool-fee 25|50|100` (bps; add `--pool-fee-from <bps> --pool-fee-minutes <min>` for an anti-snipe fall to that rate, at most 2500 bps over 30 minutes), `--pool-open legacy|curve_price` (preset tiers) and `--exp-pool-shape full_range` (exp- curves). For Solana preset tiers, omitting `--pool-open` defaults to `curve_price` (opens at the curve's last price and burns the remaining base) when `stakerAllocationBps` plus the team allocation fit the tier's limit (`graduation.maxStakerCarveBps` in the dry run); otherwise it falls back to `legacy` without refusal. Use `--pool-open legacy` to opt out. Explicit `curve_price` is refused above the limit. Experimental curves and Hood are unchanged. `--dry-run` prints Candle's preview of the launch (the curve's terms, the first buy's share, the fees, how the pool opens at graduation, the dev-buy ceiling) and builds, signs and records nothing; with `--image` the upload still happens. |
126
126
  | `candle image upload <path>` | Uploads a PNG, JPEG, GIF or WebP file, at most 10MB, and prints the `https://` URL Candle hosts it at, ready to be a launch's `imageUrl`. The type is read from the file's first bytes. The file becomes public and permanent. Needs `launch:write`; spends nothing and does not count against the launch cap. `--json` adds the content type and the size. |
127
127
  | `candle pnl [--profile <name>]` | P&L: realized net, fees, unrealized and total, then open positions, the same figures the console shows. Without `--profile`, the account's books (every profile, the web app and the CLI, one ledger; needs the Read scope), with a By agent table (each key, then Manual, then the account total). With `--profile <name>`, that profile's key's share of the same P&L: realized on its sales and the open positions that belong to it, with a row per wallet that sums to the total. Each wallet is its own average-cost pool, and a recorded move between the account's wallets carries its cost. Both show a Solana block, a Hood block, then the combined figure, the closed positions as money made, minus money lost, plus partial sells, and a `CHAIN` column (and `WALLET`, where the API names it) on positions. Unpriced positions are never valued at zero; dust (worth under one cent) is marked and still counted. A bridge between Solana and Hood is not a trade, and bridge and Relay costs are not netted. DAMM v2 LP positions are included: realized (withdrawals against the cost basis at the add, plus claimed fees), unrealized (open positions at their share of the pool plus unclaimed fees, against that basis) and vs holding (what the deposited tokens would be worth held); `Total` stays the console's token figure and `Total with LP` covers tokens and LP. |
128
128
  | `candle portfolio [<filter>] [--chain solana\|hood] [--rpc-url <url>] [--evm-rpc-url <url>] [--no-perps]` | Vault, TEE and embedded wallets on Solana and Hood in one table with chain, amount, price and value, subtotals per group and per chain, and a total. Solana vault and external balances are read over your own Solana RPC; Candle is sent only the mints, for prices. EVM vault keys are read over your own Hood RPC (`--evm-rpc-url`, else `CANDLE_EVM_RPC_URL`, else the built-in Hood RPC) for ETH and USDG only, priced by asset, never by address; an RPC on another chain is refused. An unpriced Hood token shows why (`no-market-row`, `stale-mark`, ...). `--chain` shows one chain. Each TEE wallet's DAMM v2 LP positions follow the tokens, valued by Candle at the same marks, and count in the total; one whose pool could not be read is shown as not read and the total says it is partial. A Hyperliquid section follows (each EVM TEE wallet's account, read from Hyperliquid directly, never in the total); `--no-perps` skips it. |
@@ -454,7 +454,7 @@ all returned token warnings. Missing price impact is explicitly **unavailable**,
454
454
  JSON result containing the quote; the preview and operation id go to stderr. Launch creation
455
455
  has no price impact or token receipt and shows its maximum SOL debit. Without `--buy` it makes
456
456
  no first buy; use a separate swap for that buy. With `--buy <amount>` (whole units of the quote
457
- asset, as `--amount` is for a swap) the same wallet buys at launch: in the launch transaction on
457
+ asset, as `--amount` is for a swap) the same wallet buys at launch: as its own leg in one Jito bundle with the launch on
458
458
  Solana, as a last leg after the curve and the fee on Hood. Candle reserves the buy against the
459
459
  key's spend window before anything is signed. `--buy` needs the server's
460
460
  `TEE_LAUNCH_BUYS_ENABLED=1`, which is off by default; while it is off Candle refuses the buy
package/dist/index.js CHANGED
@@ -37405,7 +37405,7 @@ var init_convert = __esm(() => {
37405
37405
  });
37406
37406
 
37407
37407
  // ../mcp/src/version.ts
37408
- var SERVER_VERSION = "0.10.21";
37408
+ var SERVER_VERSION = "0.10.22";
37409
37409
 
37410
37410
  // ../mcp/src/update-notice.ts
37411
37411
  function newer(a, b) {
@@ -37954,7 +37954,7 @@ async function executeTrade(args, cfg, doFetch) {
37954
37954
  const wrapped = JSON.stringify({ clientTradeId, resolved, api: posted.body });
37955
37955
  return posted.ok ? { text: wrapped } : { text: wrapped, isError: true };
37956
37956
  }
37957
- async function executeLaunchAndSeed(args, cfg, doFetch, imageDir, imageFs) {
37957
+ async function executeLaunchAndSeed(args, cfg, doFetch, imageDir, imageFs, opts = {}) {
37958
37958
  const apiKey = requireApiKey(cfg);
37959
37959
  const clientLaunchId = args.clientLaunchId ?? randomUUID3();
37960
37960
  const { devBuy, dryRun, buyAmount: _rawBuyAmount, ...imageFields } = args;
@@ -37968,6 +37968,12 @@ async function executeLaunchAndSeed(args, cfg, doFetch, imageDir, imageFs) {
37968
37968
  return converted.err;
37969
37969
  buyAmount = converted.raw;
37970
37970
  }
37971
+ const atomic = buyAmount !== undefined && (args.chain ?? "solana") === "solana" && !opts.headlessSeed;
37972
+ if (atomic) {
37973
+ const refused2 = seedCeilingRefusal(args.quoteAsset ?? "sol", args.mode, buyAmount, clientLaunchId);
37974
+ if (refused2)
37975
+ return refused2;
37976
+ }
37971
37977
  const image = await resolveLaunchImage(imageFields, imageDir, cfg, doFetch, imageFs);
37972
37978
  if ("refused" in image)
37973
37979
  return { text: imageRefusalBody(image.refused, { clientLaunchId }), isError: true };
@@ -37981,16 +37987,20 @@ async function executeLaunchAndSeed(args, cfg, doFetch, imageDir, imageFs) {
37981
37987
  return { text: JSON.stringify({ clientLaunchId, api }), isError: true };
37982
37988
  }
37983
37989
  const uploaded = uploadedFields(image);
37990
+ const transportFailure = (thrown) => {
37991
+ const failed = transportError("clientLaunchId", clientLaunchId, thrown);
37992
+ return Object.keys(uploaded).length === 0 ? failed : { ...failed, text: JSON.stringify({ ...JSON.parse(failed.text), ...uploaded }) };
37993
+ };
37994
+ if (atomic)
37995
+ return launchAndSeedAtomic({ ...image.args, clientLaunchId }, buyAmount, { clientLaunchId, uploaded, dryRun: dryRun === true, transportFailure }, cfg, doFetch);
37984
37996
  const path = dryRun ? "/api/v1/launch/headless/dry-run" : "/api/v1/launch/headless";
37985
37997
  const posted = await postJson(`${base(cfg)}${path}`, apiKey, {
37986
37998
  ...image.args,
37987
37999
  clientLaunchId,
37988
38000
  ...buyAmount !== undefined ? { buyAmount } : {}
37989
38001
  }, doFetch);
37990
- if ("thrown" in posted) {
37991
- const failed = transportError("clientLaunchId", clientLaunchId, posted.thrown);
37992
- return Object.keys(uploaded).length === 0 ? failed : { ...failed, text: JSON.stringify({ ...JSON.parse(failed.text), ...uploaded }) };
37993
- }
38002
+ if ("thrown" in posted)
38003
+ return transportFailure(posted.thrown);
37994
38004
  if (!posted.ok) {
37995
38005
  return { text: JSON.stringify({ clientLaunchId, ...uploaded, api: posted.body }), isError: true };
37996
38006
  }
@@ -37998,25 +38008,109 @@ async function executeLaunchAndSeed(args, cfg, doFetch, imageDir, imageFs) {
37998
38008
  return { text: JSON.stringify({ clientLaunchId, dryRun: true, ...uploaded, api: posted.body }) };
37999
38009
  }
38000
38010
  const launch = posted.body;
38001
- let market = null;
38002
- let note;
38011
+ const { market, note } = await readLaunchedMarket(launch.chain ?? "solana", launch.mint, cfg, doFetch);
38012
+ return {
38013
+ text: JSON.stringify({ clientLaunchId, ...uploaded, launch, market, ...note ? { note } : {} })
38014
+ };
38015
+ }
38016
+ async function readLaunchedMarket(chain2, mint, cfg, doFetch) {
38017
+ const note = "launch confirmed; the follow-up market read failed, read it via candle_get_market";
38003
38018
  try {
38004
- if (launch.mint === undefined)
38019
+ if (mint === undefined)
38005
38020
  throw new Error("the launch response carried no mint");
38006
- const marketRes = await doFetch(`${base(cfg)}/api/v1/markets/${encodeURIComponent(launch.chain ?? "solana")}/${encodeURIComponent(launch.mint)}`, {
38021
+ const marketRes = await doFetch(`${base(cfg)}/api/v1/markets/${encodeURIComponent(chain2)}/${encodeURIComponent(mint)}`, {
38007
38022
  method: "GET",
38008
38023
  headers: headers()
38009
38024
  });
38010
- if (marketRes.ok) {
38011
- market = JSON.parse(await marketRes.text()).market ?? null;
38012
- } else {
38013
- note = "launch confirmed; the follow-up market read failed, read it via candle_get_market";
38014
- }
38025
+ if (!marketRes.ok)
38026
+ return { market: null, note };
38027
+ return { market: JSON.parse(await marketRes.text()).market ?? null };
38015
38028
  } catch {
38016
- note = "launch confirmed; the follow-up market read failed, read it via candle_get_market";
38029
+ return { market: null, note };
38030
+ }
38031
+ }
38032
+ function seedCeilingRefusal(quoteAsset, mode, buyAmount, clientLaunchId) {
38033
+ const quote = quoteAsset.toLowerCase();
38034
+ const ceiling = SOLANA_SEED_CEILINGS[quote];
38035
+ if (!ceiling)
38036
+ return;
38037
+ const expCap = ceiling.expRaw;
38038
+ const preset = mode === "exp-a" || mode === "exp-b" || mode === "exp-c";
38039
+ const effective = preset && expCap !== undefined && expCap < ceiling.raw ? expCap : ceiling.raw;
38040
+ if (BigInt(buyAmount) <= effective)
38041
+ return;
38042
+ if (preset && expCap !== undefined) {
38043
+ return {
38044
+ text: JSON.stringify({
38045
+ clientLaunchId,
38046
+ success: false,
38047
+ error: {
38048
+ code: "DEV_BUY_TOO_HIGH",
38049
+ message: `buyAmount exceeds this launch's dev-buy ceiling of ${effective} ${ceiling.unit}: at most ${ceiling.raw} ${ceiling.unit}, or 10% of the threshold when that is lower`,
38050
+ field: "devBuy",
38051
+ retryable: false
38052
+ }
38053
+ }),
38054
+ isError: true
38055
+ };
38017
38056
  }
38018
38057
  return {
38019
- text: JSON.stringify({ clientLaunchId, ...uploaded, launch, market, ...note ? { note } : {} })
38058
+ text: JSON.stringify({
38059
+ clientLaunchId,
38060
+ success: false,
38061
+ error: {
38062
+ code: "DEV_BUY_TOO_HIGH",
38063
+ message: `buyAmount exceeds the per-launch dev-buy ceiling of ${effective} ${ceiling.unit} for ${quote} on solana`,
38064
+ field: "devBuy",
38065
+ retryable: false
38066
+ }
38067
+ }),
38068
+ isError: true
38069
+ };
38070
+ }
38071
+ async function launchAndSeedAtomic(launchBody, buyAmount, ctx, cfg, doFetch) {
38072
+ const apiKey = requireApiKey(cfg);
38073
+ const { clientLaunchId, uploaded } = ctx;
38074
+ const failed = (api, extra = {}) => ({
38075
+ text: JSON.stringify({ clientLaunchId, ...uploaded, ...extra, api }),
38076
+ isError: true
38077
+ });
38078
+ const body = { ...launchBody, payer: MAIN_PAYER, firstBuys: [{ payer: MAIN_PAYER, amountRaw: buyAmount }] };
38079
+ const built = await postJson(`${base(cfg)}/api/v1/launch/atomic/${ctx.dryRun ? "dry-run" : "build"}`, apiKey, body, doFetch);
38080
+ if ("thrown" in built)
38081
+ return ctx.transportFailure(built.thrown);
38082
+ if (!built.ok)
38083
+ return failed(built.body);
38084
+ if (ctx.dryRun)
38085
+ return { text: JSON.stringify({ clientLaunchId, dryRun: true, ...uploaded, api: built.body }) };
38086
+ const bundle = built.body;
38087
+ if (typeof bundle.bundleId !== "string" || !Array.isArray(bundle.legs) || bundle.legs.length !== 2 || bundle.legs.some((leg) => leg?.signer !== "server"))
38088
+ return failed(built.body, {
38089
+ success: false,
38090
+ error: {
38091
+ code: "MCP_INVALID_RESPONSE",
38092
+ message: "Candle did not build this launch and its seed as two legs it signs from the embedded wallet; nothing was sent. Retry with the same clientLaunchId to build it again."
38093
+ }
38094
+ });
38095
+ const bundleId = bundle.bundleId;
38096
+ const submitted = await postJson(`${base(cfg)}/api/v1/launch/atomic/submit`, apiKey, { bundleId, signedTxsBase64: [] }, doFetch);
38097
+ if ("thrown" in submitted)
38098
+ return ctx.transportFailure(submitted.thrown);
38099
+ const outcome = submitted.body;
38100
+ if (!submitted.ok || outcome.status !== "landed") {
38101
+ const next = outcome.status === "failed" ? "Neither the launch nor the seed is on chain. Retry with the same clientLaunchId: Candle starts a clean attempt under it." : outcome.status === "timeout" ? "The bundle is unresolved and may still land. Call candle_get_operation with this clientLaunchId and kind launch; never relaunch under a new id, which would be a second token." : undefined;
38102
+ return failed(submitted.body, { bundleId, ...next ? { next } : {} });
38103
+ }
38104
+ const { market, note } = await readLaunchedMarket("solana", outcome.mint, cfg, doFetch);
38105
+ return {
38106
+ text: JSON.stringify({
38107
+ clientLaunchId,
38108
+ bundleId,
38109
+ ...uploaded,
38110
+ launch: submitted.body,
38111
+ market,
38112
+ ...note ? { note } : {}
38113
+ })
38020
38114
  };
38021
38115
  }
38022
38116
  async function executeSweep(args, cfg, doFetch) {
@@ -38127,11 +38221,17 @@ async function executionStatus(cfg, doFetch) {
38127
38221
  ...unreadable.length > 0 ? { isError: true } : {}
38128
38222
  };
38129
38223
  }
38130
- var SWEEP_BASE_ASSETS;
38224
+ var SOLANA_SEED_CEILINGS, MAIN_PAYER, SWEEP_BASE_ASSETS;
38131
38225
  var init_orchestrate = __esm(() => {
38132
38226
  init_convert();
38133
38227
  init_image();
38134
38228
  init_update_notice();
38229
+ SOLANA_SEED_CEILINGS = {
38230
+ sol: { raw: 500000000n, unit: "lamports", expRaw: 500000000n },
38231
+ usdc: { raw: 500000000n, unit: "USDC raw units" },
38232
+ cndl: { raw: 2000000000000n, unit: "CNDL raw units", expRaw: 33000000000n }
38233
+ };
38234
+ MAIN_PAYER = { type: "main" };
38135
38235
  SWEEP_BASE_ASSETS = {
38136
38236
  solana: ["USDC", "CNDL", "SOL"],
38137
38237
  hood: ["USDG", "ETH"]
@@ -42567,10 +42667,12 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
42567
42667
  });
42568
42668
  register("candle_launch_and_seed", {
42569
42669
  title: "Launch a token and seed it",
42570
- description: "Launch a new token from the account's embedded wallet, with an optional dev-buy seed (in the " + "launch transaction on solana, a best-effort follow-up on hood). Works on every plan, Free included (this is not the atomic launch). Then " + "return the fresh market state and token links in one result. MOVES REAL FUNDS unless " + "dryRun. Seeds above the platform dev-buy ceiling are rejected (DEV_BUY_TOO_HIGH); " + "launch, then top up with candle_trade. " + CUSTOM_CURVE_RULE + (imageDir ? IMAGE_PATH_DISCLOSURE : ""),
42670
+ description: "Launch a new token from the account's embedded wallet, with an optional dev-buy seed, then " + "return the fresh market state and token links in one result. On solana a seed makes this " + (options.headlessSeed === true ? "the seed is a separate first-buy transaction in the same Jito bundle as the launch, so " + "both land or neither does. That works on every plan, Free included, and needs no " + "swap:write. On hood the seed is " : "the atomic launch: the launch and the seed are two transactions Candle signs and sends as " + "one Jito bundle, so both land or neither does. That needs Pro or Max (TIER_REQUIRED " + "otherwise) and swap:write on the key, and the result carries bundleId. On hood the seed is ") + "a best-effort follow-up. A launch with no seed works on every plan, Free included. MOVES " + "REAL FUNDS unless dryRun; dry-run first, which spends nothing. Seeds above the platform " + "dev-buy ceiling are rejected (DEV_BUY_TOO_HIGH): at most 0.5 SOL / 500 USDC / 2,000,000 " + "CNDL, or on an exp- curve 10% of the threshold when that is lower; the server holds the " + "creator's own seed to the effective ceiling, so launch with the largest devBuy it allows " + "and top up with candle_trade. " + CUSTOM_CURVE_RULE + (imageDir ? IMAGE_PATH_DISCLOSURE : ""),
42571
42671
  inputSchema: imageDir ? localLaunchAndSeedShape : launchAndSeedShape
42572
42672
  }, async (args) => {
42573
- const result = await executeLaunchAndSeed(args, getConfig(), doFetch, imageDir);
42673
+ const result = await executeLaunchAndSeed(args, getConfig(), doFetch, imageDir, undefined, {
42674
+ headlessSeed: options.headlessSeed === true
42675
+ });
42574
42676
  return { content: [{ type: "text", text: result.text }], ...result.isError ? { isError: true } : {} };
42575
42677
  });
42576
42678
  const marketRead = createPerpsMarketExecutor(getConfig, env, doFetch);
@@ -42833,7 +42935,7 @@ var init_tools = __esm(() => {
42833
42935
  minutes: exports_external.number().positive().max(30)
42834
42936
  }).strict()
42835
42937
  ]).optional().describe("Solana only: the graduated pool's fee. { tier: 25 | 50 | 100 } is a flat fee in bps (default 50), or " + "{ startBps, endBps, minutes } is an anti-snipe fall from startBps (at most 2500, at least endBps) to " + "endBps (25, 50 or 100) over minutes (at most 30). Cannot be combined with creatorFeeBps unless the tier is 50. " + "Omit for the standard 50 bps pool"),
42836
- poolOpen: exports_external.enum(["legacy", "curve_price"]).optional().describe(`Solana preset tiers only. "curve_price" opens the pool at the curve's last price (the whole reserve is ` + "seeded and the remaining base burned); stakerAllocationBps plus the team allocation must fit the tier's limit, " + "which the dry run returns as graduation.maxStakerCarveBps. Omit for legacy"),
42938
+ poolOpen: exports_external.enum(["legacy", "curve_price"]).optional().describe(`Solana preset tiers only. "curve_price" opens the pool at the curve's last price (the whole reserve is ` + "seeded and the remaining base burned); stakerAllocationBps plus the team allocation must fit the tier's limit, " + "which the dry run returns as graduation.maxStakerCarveBps. Omitted defaults to curve_price when those " + "allocations fit, otherwise falls back to legacy without refusal. Set legacy explicitly to opt out. " + "Explicit curve_price is refused when the allocations exceed the limit"),
42837
42939
  expPoolShape: exports_external.enum(["floor", "full_range"]).optional().describe('Solana exp- curves only. "full_range" opens the pool across the whole price range. Omit for floor'),
42838
42940
  dexVersion: exports_external.string().nullable().optional().describe('"v3" or "v4"; required for hood launches'),
42839
42941
  buyAmount: exports_external.union([exports_external.string(), exports_external.number()]).optional().describe("Initial buy, in quote base units (string or number)"),
@@ -42950,7 +43052,7 @@ var init_tools = __esm(() => {
42950
43052
  ({ buyAmount: _rawBuyAmount, ...seedableLaunchShape } = launchTokenShape);
42951
43053
  seedShape = {
42952
43054
  clientLaunchId: exports_external.string().optional().describe("Idempotency key. Auto-generated when omitted and echoed in the result."),
42953
- devBuy: exports_external.string().optional().describe('Seed buy in DECIMAL units of the quote asset this launch selects (e.g. "0.25" SOL, or ' + "ETH on hood), bundled into the launch transaction itself on solana and sent as a follow-up " + "transaction on hood. Paid from the account's embedded wallet. Follows quoteAsset, which " + "defaults to sol on solana and eth on hood. Capped by the platform dev-buy ceiling; for " + "a larger seed, launch then follow with candle_trade.")
43055
+ devBuy: exports_external.string().optional().describe('Seed buy in DECIMAL units of the quote asset this launch selects (e.g. "0.25" SOL, or ' + "ETH on hood), paid from the account's embedded wallet; the tool's description says how " + "each chain sends it. Follows quoteAsset, which defaults to sol on solana and eth on hood. " + "Capped by the platform dev-buy ceiling; for a larger seed, launch then follow with candle_trade.")
42954
43056
  };
42955
43057
  launchAndSeedShape = { ...seedableLaunchShape, ...seedShape };
42956
43058
  localImageShape = {
@@ -70488,7 +70590,7 @@ init_render();
70488
70590
  init_secret_store();
70489
70591
 
70490
70592
  // src/version.ts
70491
- var CLI_VERSION = "0.11.39";
70593
+ var CLI_VERSION = "0.11.40";
70492
70594
 
70493
70595
  // src/commands/auth.ts
70494
70596
  init_keys_embedded_wallet();
@@ -71446,9 +71548,10 @@ var HELP = {
71446
71548
  launch: {
71447
71549
  group: "Trade",
71448
71550
  summary: "Create a token on Solana or Hood, with an optional first buy",
71449
- description: "Creates a token from a TEE wallet. Without --buy it makes no first buy, so the launch and the position are two decisions, and the buy is a separate candle swap. --buy <amount> adds a first buy from the same wallet, in whole units of the quote asset (SOL, USDC or CNDL on Solana, ETH on Hood; not USDG), within the dev-buy ceilings: on Solana it is its own transaction, sent with the launch as one Jito bundle (an atomic launch), so the launch and the first buy land together or not at all and the bundle pays a Jito tip; on Hood it is a last leg after the curve and the fee. On Solana it also needs the swap:write scope and a Candle deployment with a Jito block engine. Only configured raw caps and USD limits apply; unset means unlimited and zero still binds. When a USD limit is set, Candle reserves the buy against the key's spend window before anything is signed. --buy needs a server with TEE_LAUNCH_BUYS_ENABLED on, which is off by default; while it is off Candle refuses the buy and nothing is built. Needs the launch:write scope and allowLaunch on the wallet, which only the account owner can turn on (candle wallets allow-launch <wallet>, over the device token; an agent key cannot). The wallet decides the chain: a Hood TEE wallet launches on Hood, needs --dex-version, and signs one leg at a time (the curve, then the fee, then the buy with --buy), each only after the one before it landed. Launching from a TEE wallet needs Pro or Max (TIER_REQUIRED otherwise; candle plans). Every plan, Free included, can launch from its embedded wallet with an optional dev buy (same transaction on Solana; best-effort follow-up on Hood), through the MCP tool candle_launch_and_seed or the SDK's launch(); this command does not use that path. The image is one of two flags, never both: --image-url <https-url> for one already hosted, or --image <path> for a PNG, JPEG, GIF or WebP file on this machine, at most 10MB. With --image the file is uploaded to Candle first and the launch is made with the URL that comes back, which --json reports as imageUrl. The upload happens before the confirm prompt and cannot be undone: the file becomes public and permanent even if you then decline or the launch fails. Re-running with the same --client-trade-id resumes the launch without uploading again. --image-url takes only an https:// URL. --banner <file|URL> adds a wide banner: an https URL is passed through; a local file uses the same publication notice and 10MB limit, uploaded with kind=banner so it is never cropped. Banners must be wider than 1.5:1. The result reports bannerUrl, and an atomic retry reuses the saved URL. --mode picks the curve: open or exclusive (the default tier for the pair), test-open or test-exclusive where the server has ENABLE_TEST_CURVES on, or on Solana with sol or cndl the experimental curves exp-a, exp-b, exp-c and exp-custom. exp-custom takes your own curve: --curve-v <V> (the virtual quote it starts with) and --curve-g <G> (what it raises before graduating), in whole units of the quote asset, within limits Candle checks; it works only where the server has EXP_CUSTOM_CURVES on. --creator-fee-bps 100 or 200 adds an extra creator fee on an exp- mode, paid to the creator in full. On an exp- curve the first buy is at most 0.5 SOL (2,000,000 CNDL), or 10% of the threshold when that is lower. --dry-run sends the launch's body to Candle's dry run and prints the preview (the curve's terms, the first buy's share, the fees, how the pool opens at graduation and the dev-buy ceiling) without building, signing or recording anything; the id stays unused. With --image it still uploads the file, which is public and permanent.",
71551
+ description: "Creates a token from a TEE wallet. Without --buy it makes no first buy, so the launch and the position are two decisions, and the buy is a separate candle swap. --buy <amount> adds a first buy from the same wallet, in whole units of the quote asset (SOL, USDC or CNDL on Solana, ETH on Hood; not USDG), within the dev-buy ceilings: on Solana it is its own transaction, sent with the launch as one Jito bundle (an atomic launch), so the launch and the first buy land together or not at all and the bundle pays a Jito tip; on Hood it is a last leg after the curve and the fee. On Solana it also needs the swap:write scope and a Candle deployment with a Jito block engine. Only configured raw caps and USD limits apply; unset means unlimited and zero still binds. When a USD limit is set, Candle reserves the buy against the key's spend window before anything is signed. --buy needs a server with TEE_LAUNCH_BUYS_ENABLED on, which is off by default; while it is off Candle refuses the buy and nothing is built. Needs the launch:write scope and allowLaunch on the wallet, which only the account owner can turn on (candle wallets allow-launch <wallet>, over the device token; an agent key cannot). The wallet decides the chain: a Hood TEE wallet launches on Hood, needs --dex-version, and signs one leg at a time (the curve, then the fee, then the buy with --buy), each only after the one before it landed. Launching from a TEE wallet needs Pro or Max (TIER_REQUIRED otherwise; candle plans). --wallet embedded (or main) launches from the account's embedded wallet instead, on Solana, as an atomic launch: it needs --buy, the embedded wallet is the creator and the only first buyer, and Candle signs both legs and sends them as one Jito bundle, so the launch and the first buy land together or not at all. It runs Candle's dry run first, which builds, reserves and records nothing, shows that quote to confirm, and only then builds and sends; the build's first-buy floor must be at least the one shown. --dry-run stops after the preview. It needs Pro or Max, launch:write and swap:write, a key allowed the embedded wallet, and does not depend on TEE_LAUNCH_BUYS_ENABLED. Without a first buy, every plan can launch from the embedded wallet through the MCP tool candle_launch_token or the SDK's launch(). The image is one of two flags, never both: --image-url <https-url> for one already hosted, or --image <path> for a PNG, JPEG, GIF or WebP file on this machine, at most 10MB. With --image the file is uploaded to Candle first and the launch is made with the URL that comes back, which --json reports as imageUrl. The upload happens before the confirm prompt and cannot be undone: the file becomes public and permanent even if you then decline or the launch fails. Re-running with the same --client-trade-id resumes the launch without uploading again. --image-url takes only an https:// URL. --banner <file|URL> adds a wide banner: an https URL is passed through; a local file uses the same publication notice and 10MB limit, uploaded with kind=banner so it is never cropped. Banners must be wider than 1.5:1. The result reports bannerUrl, and an atomic retry reuses the saved URL. --mode picks the curve: open or exclusive (the default tier for the pair), test-open or test-exclusive where the server has ENABLE_TEST_CURVES on, or on Solana with sol or cndl the experimental curves exp-a, exp-b, exp-c and exp-custom. exp-custom takes your own curve: --curve-v <V> (the virtual quote it starts with) and --curve-g <G> (what it raises before graduating), in whole units of the quote asset, within limits Candle checks; it works only where the server has EXP_CUSTOM_CURVES on. --creator-fee-bps 100 or 200 adds an extra creator fee on an exp- mode, paid to the creator in full. On an exp- curve the first buy is at most 0.5 SOL (2,000,000 CNDL), or 10% of the threshold when that is lower. --dry-run sends the launch's body to Candle's dry run and prints the preview (the curve's terms, the first buy's share, the fees, how the pool opens at graduation and the dev-buy ceiling) without building, signing or recording anything; the id stays unused. With --image it still uploads the file, which is public and permanent.",
71450
71552
  usage: [
71451
- "candle launch --name <name> --symbol <symbol> --image <path>|--image-url <url> --wallet <tee> [--banner <file|URL>] [--buy <amount>] [--quote-asset <asset>] [--dex-version v3|v4] [--mode <mode>] [--curve-v <V> --curve-g <G>] [--creator-fee-bps 0|100|200] [--pool-fee <25|50|100>] [--pool-open <shape>] [--exp-pool-shape <shape>] [--dry-run]"
71553
+ "candle launch --name <name> --symbol <symbol> --image <path>|--image-url <url> --wallet <tee> [--banner <file|URL>] [--buy <amount>] [--quote-asset <asset>] [--dex-version v3|v4] [--mode <mode>] [--curve-v <V> --curve-g <G>] [--creator-fee-bps 0|100|200] [--pool-fee <25|50|100>] [--pool-open <shape>] [--exp-pool-shape <shape>] [--dry-run]",
71554
+ "candle launch --name <name> --symbol <symbol> --image <path>|--image-url <url> --wallet embedded --buy <amount> [--banner <file|URL>] [--quote-asset <asset>] [--dry-run]"
71452
71555
  ],
71453
71556
  rows: [],
71454
71557
  flags: [
@@ -71463,7 +71566,10 @@ var HELP = {
71463
71566
  invocation: "--banner <file|URL>",
71464
71567
  description: "A wide banner from a local file or https URL; files become public and permanent, without cropping"
71465
71568
  },
71466
- { invocation: "--wallet <tee>", description: "The TEE wallet that creates it" },
71569
+ {
71570
+ invocation: "--wallet <tee>|embedded",
71571
+ description: "The TEE wallet that creates it, or embedded (main) for the account's embedded wallet, with --buy, as an atomic launch"
71572
+ },
71467
71573
  {
71468
71574
  invocation: "--buy <amount>",
71469
71575
  description: "A first buy from the same wallet, in whole units of the quote asset; needs TEE_LAUNCH_BUYS_ENABLED on the server"
@@ -71483,7 +71589,7 @@ var HELP = {
71483
71589
  },
71484
71590
  {
71485
71591
  invocation: "--pool-open legacy|curve_price",
71486
- description: "Solana preset tiers: curve_price opens the pool at the curve's last price and burns the rest; the staker carve plus team allocation must fit the tier's limit (the dry run returns it)"
71592
+ description: "Solana preset tiers: curve_price opens the pool at the curve's last price and burns the rest. Omitted defaults to curve_price when stakerAllocationBps plus the team allocation fit the tier's limit (dry-run graduation.maxStakerCarveBps), otherwise falls back to legacy without refusal. Use --pool-open legacy to opt out. Explicit curve_price is refused above the limit"
71487
71593
  },
71488
71594
  {
71489
71595
  invocation: "--exp-pool-shape floor|full_range",
@@ -71511,6 +71617,7 @@ var HELP = {
71511
71617
  "candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet AgentOne",
71512
71618
  "candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet HoodAgent --dex-version v4",
71513
71619
  "candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet AgentOne --buy 0.1",
71620
+ "candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet embedded --buy 0.1 --dry-run",
71514
71621
  "candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet AgentOne --mode exp-custom --curve-v 4 --curve-g 20 --creator-fee-bps 100 --dry-run"
71515
71622
  ],
71516
71623
  env: ENV_API
@@ -84802,7 +84909,9 @@ function poolRequestFields(flags) {
84802
84909
 
84803
84910
  // src/commands/launch.ts
84804
84911
  init_swap();
84805
- var USAGE6 = "Usage: candle launch --name <name> --symbol <symbol> --image <path>|--image-url <https-url> --wallet <tee> [--banner <file|URL>] [--buy <amount>] [--client-trade-id <id>] [--quote-asset sol|usdc|cndl|eth|usdg] [--dex-version v3|v4] [--mode <mode>] [--curve-v <V> --curve-g <G>] [--creator-fee-bps 0|100|200] [--pool-fee <25|50|100> [--pool-fee-from <bps> --pool-fee-minutes <min>]] [--pool-open legacy|curve_price] [--exp-pool-shape floor|full_range] [--dry-run] [--rpc-url <url>] [--yes]. A Hood TEE wallet launches on Hood and needs --dex-version.";
84912
+ var USAGE6 = "Usage: candle launch --name <name> --symbol <symbol> --image <path>|--image-url <https-url> --wallet <tee>|embedded [--banner <file|URL>] [--buy <amount>] [--client-trade-id <id>] [--quote-asset sol|usdc|cndl|eth|usdg] [--dex-version v3|v4] [--mode <mode>] [--curve-v <V> --curve-g <G>] [--creator-fee-bps 0|100|200] [--pool-fee <25|50|100> [--pool-fee-from <bps> --pool-fee-minutes <min>]] [--pool-open legacy|curve_price] [--exp-pool-shape floor|full_range] [--dry-run] [--rpc-url <url>] [--yes]. A Hood TEE wallet launches on Hood and needs --dex-version.";
84913
+ var EMBEDDED_WALLET_NAMES = new Set(["embedded", "main"]);
84914
+ var EMBEDDED_USAGE = "--wallet embedded launches on Solana as an atomic launch: it needs --buy <amount>, takes --quote-asset sol, usdc or cndl, and takes no --dex-version. To launch from the embedded wallet with no first buy, use the MCP tool candle_launch_token or POST /api/v1/launch/headless.";
84806
84915
  var QUOTE_CHAIN = {
84807
84916
  sol: "solana",
84808
84917
  usdc: "solana",
@@ -84920,7 +85029,15 @@ async function launch(args, ctx) {
84920
85029
  }
84921
85030
  const flags = parsed.values;
84922
85031
  const id = flags["--client-trade-id"] ?? `launch-${randomUUID5()}`;
85032
+ const embedded = EMBEDDED_WALLET_NAMES.has(flags["--wallet"] ?? "");
84923
85033
  const dryRun = parsed.booleans.has("--dry-run");
85034
+ if (embedded) {
85035
+ const quote = flags["--quote-asset"]?.toLowerCase();
85036
+ if (flags["--buy"] === undefined || flags["--dex-version"] !== undefined || quote && QUOTE_CHAIN[quote] !== "solana") {
85037
+ writeUsageFailure(ctx.deps, `${EMBEDDED_USAGE} ${USAGE6}`, ctx.json);
85038
+ return 2;
85039
+ }
85040
+ }
84924
85041
  const curveProblem = curveFlagProblem(flags);
84925
85042
  if (curveProblem) {
84926
85043
  writeUsageFailure(ctx.deps, `${curveProblem} ${USAGE6}`, ctx.json);
@@ -84991,6 +85108,16 @@ async function launch(args, ctx) {
84991
85108
  return printTradingResult(ctx, prior);
84992
85109
  }
84993
85110
  }
85111
+ if (embedded)
85112
+ return await embeddedLaunch(ctx, key, {
85113
+ flags,
85114
+ id,
85115
+ image,
85116
+ banner,
85117
+ retry,
85118
+ yes: parsed.booleans.has("--yes"),
85119
+ dryRun
85120
+ });
84994
85121
  const wallet3 = await tradingWallet(ctx, key, flags["--wallet"], "launch:write", hinted ?? "any");
84995
85122
  const chain2 = wallet3.chain === "evm" ? "hood" : "solana";
84996
85123
  if (chain2 === "hood" && flags["--dex-version"] === undefined)
@@ -85000,38 +85127,10 @@ async function launch(args, ctx) {
85000
85127
  if (atomic)
85001
85128
  atomicPreflight(ctx, flags, wallet3, buy);
85002
85129
  const solana = chain2 === "solana" && !atomic && !dryRun ? await tradingSolanaClient(ctx, flags["--rpc-url"]) : undefined;
85003
- let imageUrl = flags["--image-url"] ?? "";
85004
- if (image && retry?.imageUrl)
85005
- imageUrl = retry.imageUrl;
85006
- else if (image) {
85007
- ctx.deps.stderr.write(uploadNotice(image));
85008
- try {
85009
- imageUrl = await uploadImage(ctx, key, image);
85010
- } catch (error) {
85011
- ctx.deps.stderr.write(`The launch was not started, and its id ${id} is unused.
85012
- `);
85013
- return tradingFailure(ctx, error);
85014
- }
85015
- ctx.deps.stderr.write(`Image: ${safeText(imageUrl)}
85016
- `);
85017
- }
85018
- let bannerUrl = retry ? retry.bannerUrl : flags["--banner"]?.startsWith("https://") ? flags["--banner"] : undefined;
85019
- if (banner && !retry) {
85020
- ctx.deps.stderr.write(uploadNotice(banner));
85021
- try {
85022
- bannerUrl = await uploadImage(ctx, key, banner, "banner");
85023
- } catch (error) {
85024
- ctx.deps.stderr.write(`The launch was not started, and its id ${id} is unused.
85025
- `);
85026
- return tradingFailure(ctx, error);
85027
- }
85028
- ctx.deps.stderr.write(`Banner: ${safeText(bannerUrl)}
85029
- `);
85030
- }
85031
- const used = {
85032
- ...image ? { imageUrl: safeText(imageUrl) } : {},
85033
- ...bannerUrl ? { bannerUrl: safeText(bannerUrl) } : {}
85034
- };
85130
+ const uploaded = await uploadLaunchImages(ctx, key, { flags, id, image, banner, retry });
85131
+ if (typeof uploaded === "number")
85132
+ return uploaded;
85133
+ const { imageUrl, bannerUrl, used } = uploaded;
85035
85134
  if (dryRun)
85036
85135
  return await dryRunLaunch(ctx, key, { flags, id, wallet: wallet3, chain: chain2, atomic, buy, imageUrl, bannerUrl, used });
85037
85136
  if (atomic)
@@ -85108,6 +85207,42 @@ async function launch(args, ctx) {
85108
85207
  return tradingFailure(ctx, error, dryRun ? undefined : id);
85109
85208
  }
85110
85209
  }
85210
+ async function uploadLaunchImages(ctx, key, args) {
85211
+ const { flags, id, image, banner, retry } = args;
85212
+ let imageUrl = flags["--image-url"] ?? "";
85213
+ if (image && retry?.imageUrl)
85214
+ imageUrl = retry.imageUrl;
85215
+ else if (image) {
85216
+ ctx.deps.stderr.write(uploadNotice(image));
85217
+ try {
85218
+ imageUrl = await uploadImage(ctx, key, image);
85219
+ } catch (error) {
85220
+ ctx.deps.stderr.write(`The launch was not started, and its id ${id} is unused.
85221
+ `);
85222
+ return tradingFailure(ctx, error);
85223
+ }
85224
+ ctx.deps.stderr.write(`Image: ${safeText(imageUrl)}
85225
+ `);
85226
+ }
85227
+ let bannerUrl = retry ? retry.bannerUrl : flags["--banner"]?.startsWith("https://") ? flags["--banner"] : undefined;
85228
+ if (banner && !retry) {
85229
+ ctx.deps.stderr.write(uploadNotice(banner));
85230
+ try {
85231
+ bannerUrl = await uploadImage(ctx, key, banner, "banner");
85232
+ } catch (error) {
85233
+ ctx.deps.stderr.write(`The launch was not started, and its id ${id} is unused.
85234
+ `);
85235
+ return tradingFailure(ctx, error);
85236
+ }
85237
+ ctx.deps.stderr.write(`Banner: ${safeText(bannerUrl)}
85238
+ `);
85239
+ }
85240
+ const used = {
85241
+ ...image ? { imageUrl: safeText(imageUrl) } : {},
85242
+ ...bannerUrl ? { bannerUrl: safeText(bannerUrl) } : {}
85243
+ };
85244
+ return { imageUrl, ...bannerUrl ? { bannerUrl } : {}, used };
85245
+ }
85111
85246
  function launchFields(args) {
85112
85247
  const { flags } = args;
85113
85248
  return {
@@ -85237,6 +85372,11 @@ function expThresholdRaw(flags, quoteAsset) {
85237
85372
  return mode === "exp-a" || mode === "exp-b" || mode === "exp-c" ? EXP_PRESET_THRESHOLD_RAW[quoteAsset] : undefined;
85238
85373
  }
85239
85374
  function atomicPreflight(ctx, flags, wallet3, buy) {
85375
+ firstBuyCeiling(ctx, flags, buy);
85376
+ if (wallet3.scopes !== undefined && !wallet3.scopes.includes("swap:write"))
85377
+ throw swapScopeRefusal();
85378
+ }
85379
+ function firstBuyCeiling(ctx, flags, buy) {
85240
85380
  const quoteAsset = (flags["--quote-asset"] ?? "sol").toLowerCase();
85241
85381
  const { ceiling, unit } = devBuyCeiling(ctx.deps.env, quoteAsset);
85242
85382
  const threshold = expThresholdRaw(flags, quoteAsset);
@@ -85251,8 +85391,6 @@ function atomicPreflight(ctx, flags, wallet3, buy) {
85251
85391
  }
85252
85392
  if (BigInt(buy.raw) > ceiling)
85253
85393
  throw new TradingError("DEV_BUY_TOO_HIGH", `buyAmount exceeds the per-launch dev-buy ceiling of ${ceiling} ${unit} for ${quoteAsset} on solana`, { details: { field: "buyAmount" } });
85254
- if (wallet3.scopes !== undefined && !wallet3.scopes.includes("swap:write"))
85255
- throw swapScopeRefusal();
85256
85394
  }
85257
85395
  function swapScopeRefusal() {
85258
85396
  return new TradingError("SCOPE_MISSING", "This key needs swap:write for a first buy on a TEE launch: the buy is its own leg in an atomic launch. Nothing was built or signed.", {
@@ -85363,28 +85501,33 @@ async function atomicLaunch(ctx, key, args) {
85363
85501
  await saveOperationBundle(ctx, key, id, bundle);
85364
85502
  ctx.deps.stderr.write(`Bundle: ${safeText(built.bundleId)} (launch ${signatures[0]}, first buy ${signatures[1]})
85365
85503
  `);
85504
+ return await submitBundle(ctx, key, { id, bundle, signed, quote, used: args.used });
85505
+ }
85506
+ async function submitBundle(ctx, key, args) {
85507
+ const { id, bundle, signed, quote } = args;
85508
+ const { bundleId, signatures } = bundle;
85366
85509
  const submitted = await apiRequest("/api/v1/launch/atomic/submit", {
85367
85510
  apiUrl: ctx.apiUrl,
85368
85511
  credentials: { apiKey: key },
85369
85512
  auth: "key",
85370
85513
  method: "POST",
85371
- body: { bundleId: built.bundleId, signedTxsBase64: signed },
85514
+ body: { bundleId, signedTxsBase64: signed },
85372
85515
  fetch: ctx.deps.fetch,
85373
85516
  env: ctx.deps.env
85374
85517
  });
85375
85518
  const outcome = atomicOutcomeSchema.safeParse(submitted.ok ? submitted.body : submitted.raw);
85376
- const details = { bundleId: built.bundleId, signatures };
85377
- if (!outcome.success || outcome.data.bundleId !== built.bundleId) {
85519
+ const details = { bundleId, signatures };
85520
+ if (!outcome.success || outcome.data.bundleId !== bundleId) {
85378
85521
  const refused2 = !submitted.ok && submitted.status >= 400 && submitted.status < 500 && submitted.status !== 409;
85379
85522
  if (refused2)
85380
85523
  await saveOperationBundle(ctx, key, id, { ...bundle, outcome: "refused" });
85381
- throw new TradingError(submitted.ok ? "INVALID_RESPONSE" : submitted.code ?? "REQUEST_FAILED", refused2 ? `Candle refused bundle ${safeText(built.bundleId)} and sent nothing: ${safeText(submitted.ok ? "" : submitted.message)}. Re-run with the same --client-trade-id ${id} to build it again.` : `Bundle ${safeText(built.bundleId)} may have been sent, and its outcome is unknown${submitted.ok ? "" : `: ${safeText(submitted.message)}`}. Re-run with the same --client-trade-id ${id} to read it; nothing is re-signed or re-sent. Never relaunch under a new id: that would be a second token.`, { ...refused2 ? {} : { exitCode: 3 }, details });
85524
+ throw new TradingError(submitted.ok ? "INVALID_RESPONSE" : submitted.code ?? "REQUEST_FAILED", refused2 ? `Candle refused bundle ${safeText(bundleId)} and sent nothing: ${safeText(submitted.ok ? "" : submitted.message)}. Re-run with the same --client-trade-id ${id} to build it again.` : `Bundle ${safeText(bundleId)} may have been sent, and its outcome is unknown${submitted.ok ? "" : `: ${safeText(submitted.message)}`}. Re-run with the same --client-trade-id ${id} to read it; nothing is re-signed or re-sent. Never relaunch under a new id: that would be a second token.`, { ...refused2 ? {} : { exitCode: 3 }, details });
85382
85525
  }
85383
85526
  await saveOperationBundle(ctx, key, id, { ...bundle, outcome: outcome.data.status });
85384
85527
  if (outcome.data.status === "failed")
85385
- throw new TradingError("BUNDLE_FAILED", `Bundle ${safeText(built.bundleId)} did not land: neither the launch nor its first buy is on chain. Retry with the same --client-trade-id ${id}; Candle starts a clean attempt under it.`, { details });
85528
+ throw new TradingError("BUNDLE_FAILED", `Bundle ${safeText(bundleId)} did not land: neither the launch nor its first buy is on chain. Retry with the same --client-trade-id ${id}; Candle starts a clean attempt under it.`, { details });
85386
85529
  if (outcome.data.status === "timeout")
85387
- throw new TradingError("BUNDLE_UNRESOLVED", `Bundle ${safeText(built.bundleId)} is unresolved and may still land. Re-run with the same --client-trade-id ${id} to read its outcome; nothing is re-signed or re-sent. Never relaunch under a new id: that would be a second token.`, { exitCode: 3, details });
85530
+ throw new TradingError("BUNDLE_UNRESOLVED", `Bundle ${safeText(bundleId)} is unresolved and may still land. Re-run with the same --client-trade-id ${id} to read its outcome; nothing is re-signed or re-sent. Never relaunch under a new id: that would be a second token.`, { exitCode: 3, details });
85388
85531
  const [launchSignature, buySignature] = outcome.data.signatures;
85389
85532
  ctx.deps.stderr.write(`Launched ${safeText(outcome.data.mint)}. Launch signature: ${safeText(launchSignature)}. First buy signature: ${safeText(buySignature)}.
85390
85533
  `);
@@ -85408,6 +85551,127 @@ function resumeAtomicLaunch(ctx, id, local, prior) {
85408
85551
  return printTradingResult(ctx, { ...prior, ...saved });
85409
85552
  throw new TradingError("OPERATION_IN_FLIGHT", `Launch ${id}'s bundle ${safeText(local.bundleId)} is unresolved (the launch job is ${safeText(prior.job.status)}) and may still land. Nothing was re-signed or re-sent. Re-run with the same --client-trade-id to read it again; never relaunch under a new id, which would be a second token.`, { exitCode: 3, details: { ...saved, job: prior.job } });
85410
85553
  }
85554
+ var EMBEDDED_PAYER = { type: "main" };
85555
+ var atomicDryRunSchema = exports_external.object({
85556
+ launchWallet: exports_external.string(),
85557
+ launchSigner: exports_external.enum(["server", "client"]),
85558
+ fills: exports_external.array(exports_external.object({
85559
+ index: exports_external.number().int(),
85560
+ signer: exports_external.enum(["server", "client"]),
85561
+ wallet: exports_external.string(),
85562
+ amountRaw: exports_external.string().regex(/^\d+$/),
85563
+ feeRaw: exports_external.string().regex(/^\d+$/),
85564
+ expectedFill: exports_external.object({
85565
+ amountOutRaw: exports_external.string().regex(/^\d+$/),
85566
+ minAmountOutRaw: exports_external.string().regex(/^\d+$/)
85567
+ }),
85568
+ supplyPct: exports_external.number()
85569
+ }).passthrough()),
85570
+ graduatesInBundle: exports_external.boolean().optional()
85571
+ }).passthrough();
85572
+ async function embeddedLaunchWallet(ctx, key) {
85573
+ const discovered = embeddedSchema.parse(await request(ctx, key, "/api/v1/agent/wallets/embedded"));
85574
+ const address = discovered.wallets?.solana?.address;
85575
+ if (!address)
85576
+ throw new TradingError("PAYER_REQUIRED", "This account has no Solana embedded wallet. Create one in the app, or launch from a Solana TEE wallet with --wallet <tee>. Nothing was built.");
85577
+ if (discovered.embeddedWalletPermission === "denied")
85578
+ throw new TradingError("EMBEDDED_WALLET_NOT_PERMITTED", `This key may not use the account's embedded wallet (${address}). Nothing was built.`, {
85579
+ suggestion: "Have the owner allow it: candle keys update <prefix> --embedded-wallet allow (device token), or in the web key manager. Or launch from a TEE wallet bound to this key with --wallet <tee>."
85580
+ });
85581
+ return address;
85582
+ }
85583
+ function embeddedRefusal(error) {
85584
+ if (!(error instanceof TradingError))
85585
+ return error;
85586
+ if (error.code === "SCOPE_MISSING")
85587
+ return new TradingError("SCOPE_MISSING", "This key needs swap:write as well as launch:write: the first buy is its own leg in the atomic launch. Nothing was built or signed.", { suggestion: "Add swap:write to the key, then run the same command again." });
85588
+ if (error.code === "VALIDATION_FAILED" && error.message.includes("configured Jito block engine"))
85589
+ return new TradingError("VALIDATION_FAILED", "An embedded-wallet launch goes to Candle as one Jito bundle, and this Candle deployment has no Jito block engine configured. Nothing was built or signed.");
85590
+ return error;
85591
+ }
85592
+ async function embeddedLaunch(ctx, key, args) {
85593
+ const { flags, id } = args;
85594
+ const buy = launchBuy(flags, "solana");
85595
+ firstBuyCeiling(ctx, flags, buy);
85596
+ const wallet3 = await embeddedLaunchWallet(ctx, key);
85597
+ const uploaded = await uploadLaunchImages(ctx, key, args);
85598
+ if (typeof uploaded === "number")
85599
+ return uploaded;
85600
+ const { imageUrl, bannerUrl, used } = uploaded;
85601
+ const body = {
85602
+ clientLaunchId: id,
85603
+ chain: "solana",
85604
+ name: flags["--name"],
85605
+ symbol: flags["--symbol"],
85606
+ imageUrl,
85607
+ ...bannerUrl ? { bannerUrl } : {},
85608
+ ...flags["--description"] ? { description: flags["--description"] } : {},
85609
+ ...flags["--quote-asset"] ? { quoteAsset: flags["--quote-asset"] } : {},
85610
+ ...flags["--mode"] ? { mode: flags["--mode"] } : {},
85611
+ ...launchCurveFields(flags),
85612
+ ...poolRequestFields(flags),
85613
+ payer: EMBEDDED_PAYER,
85614
+ firstBuys: [{ payer: EMBEDDED_PAYER, amountRaw: buy.raw }]
85615
+ };
85616
+ const answer2 = await request(ctx, key, "/api/v1/launch/atomic/dry-run", body).catch((error) => {
85617
+ throw embeddedRefusal(error);
85618
+ });
85619
+ const preview = atomicDryRunSchema.safeParse(answer2);
85620
+ const fill = preview.success ? preview.data.fills[0] : undefined;
85621
+ if (!preview.success || !fill || preview.data.fills.length !== 1 || preview.data.launchWallet !== wallet3 || preview.data.launchSigner !== "server" || fill.wallet !== wallet3 || fill.signer !== "server" || fill.amountRaw !== buy.raw)
85622
+ throw new TradingError("INVALID_RESPONSE", "Candle's dry run does not read as this launch and its first buy from the embedded wallet; nothing was built.");
85623
+ const quoteAsset = (flags["--quote-asset"] ?? "sol").toUpperCase();
85624
+ const decimals = BASES[quoteAsset]?.decimals ?? 9;
85625
+ const quote = {
85626
+ intent: `Launch ${flags["--name"]} (${flags["--symbol"]}) with a first buy of ${buy.display} from the embedded wallet, as one Jito bundle`,
85627
+ wallet: wallet3,
85628
+ venue: "curve launch (atomic bundle, signed by Candle)",
85629
+ priceImpactPct: null,
85630
+ candleFee: fill.feeRaw !== "0" ? `${decimalAmount(fill.feeRaw, decimals)} ${quoteAsset} (the tier fee on the first buy, in its leg)` : "none on this first buy",
85631
+ minimumReceived: `${fill.expectedFill.minAmountOutRaw} raw units of the new token (the first buy's floor; ${fill.supplyPct}% of the supply expected)`,
85632
+ tokenRisks: []
85633
+ };
85634
+ if (args.dryRun) {
85635
+ ctx.deps.stderr.write(`Dry run: nothing was built, signed or recorded, and launch id ${id} is unused.
85636
+ `);
85637
+ return printTradingResult(ctx, { ...answer2, clientTradeId: id, kind: "launch", quote, ...used });
85638
+ }
85639
+ ctx.deps.stderr.write(`Launch creates the token, then spends ${buy.display} on a first buy from the embedded wallet as its own transaction. Both go to Candle as one Jito bundle: the launch and the first buy land together or not at all. Candle signs both, and the bundle pays a Jito tip from this wallet inside the launch transaction, beside the launch's rent and network fees. Price impact does not apply to creation.
85640
+ `);
85641
+ if (preview.data.graduatesInBundle)
85642
+ ctx.deps.stderr.write(`This first buy alone takes the new curve to graduation inside the bundle.
85643
+ `);
85644
+ if (!await confirmQuote(ctx, quote, args.yes))
85645
+ return printTradingResult(ctx, {
85646
+ success: true,
85647
+ status: "cancelled",
85648
+ clientTradeId: id,
85649
+ kind: "launch",
85650
+ quote,
85651
+ ...used
85652
+ });
85653
+ if (!args.retry && !await claimOperation(ctx, key, id, "launch"))
85654
+ throw new TradingError("OPERATION_ALREADY_STARTED", "This machine already started this launch id; no write was resent.");
85655
+ ctx.deps.stderr.write(`Operation: ${id}
85656
+ `);
85657
+ const built = atomicBuildSchema.parse(await request(ctx, key, "/api/v1/launch/atomic/build", body).catch((error) => {
85658
+ throw embeddedRefusal(error);
85659
+ }));
85660
+ const bundle = { bundleId: built.bundleId, signatures: [], imageUrl, ...bannerUrl ? { bannerUrl } : {} };
85661
+ await saveOperationBundle(ctx, key, id, { ...bundle, outcome: "unsigned" });
85662
+ const [launchLeg, buyLeg] = built.legs;
85663
+ if (built.legs.length !== 2 || launchLeg?.role !== "launch" || buyLeg?.role !== "buy" || built.legs.some((leg, index) => leg.index !== index || leg.signer !== "server"))
85664
+ throw new TradingError("INVALID_RESPONSE", `Candle did not build bundle ${safeText(built.bundleId)} as this launch and its first buy, both from the embedded wallet; nothing was sent. Re-run with the same --client-trade-id ${id} to build it again.`);
85665
+ const floor = buyLeg.expectedFill?.minAmountOutRaw;
85666
+ if (floor === undefined || BigInt(floor) < BigInt(fill.expectedFill.minAmountOutRaw))
85667
+ throw new TradingError("QUOTE_CHANGED", `Bundle ${safeText(built.bundleId)}'s first-buy floor (${safeText(floor ?? "none")} raw units) is below the ${fill.expectedFill.minAmountOutRaw} you confirmed; nothing was sent. Re-run with the same --client-trade-id ${id} to preview it again.`);
85668
+ if (!Number.isFinite(built.expiresAt) || built.expiresAt <= ctx.deps.now())
85669
+ throw new TradingError("QUOTE_EXPIRED", "The launch build expired before it was sent.");
85670
+ await saveOperationBundle(ctx, key, id, bundle);
85671
+ ctx.deps.stderr.write(`Bundle: ${safeText(built.bundleId)} (Candle signs the launch and the first buy)
85672
+ `);
85673
+ return await submitBundle(ctx, key, { id, bundle, signed: [], quote, used });
85674
+ }
85411
85675
  async function hoodLaunch(ctx, key, args) {
85412
85676
  const { flags, id, wallet: wallet3, buy } = args;
85413
85677
  if (Object.keys(poolRequestFields(flags)).length)
@@ -95137,7 +95401,6 @@ function stepReporter(write, tty, ascii = false) {
95137
95401
  `);
95138
95402
  return;
95139
95403
  }
95140
- write(`${FRAMES[0]} ${label}`);
95141
95404
  frame = 1;
95142
95405
  const interrupt = (signal) => {
95143
95406
  clearSpinner();
@@ -95158,6 +95421,7 @@ function stepReporter(write, tty, ascii = false) {
95158
95421
  }, INTERVAL_MS3);
95159
95422
  if (typeof timer === "object" && "unref" in timer)
95160
95423
  timer.unref();
95424
+ write(`${FRAMES[0]} ${label}`);
95161
95425
  },
95162
95426
  done(label) {
95163
95427
  clearSpinner();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@candledottv/cli",
3
- "version": "0.11.39",
3
+ "version": "0.11.40",
4
4
  "description": "The Candle CLI: authorize a device from your browser, then manage API keys, wallets, and setup health from the terminal",
5
5
  "type": "module",
6
6
  "bin": {