@candledottv/cli 0.11.16 → 0.11.17

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 +17 -10
  2. package/dist/index.js +348 -55
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @candledottv/cli
2
2
 
3
- The Candle CLI (current version 0.11.15): authorize a device from your browser, keep your own keys
3
+ The Candle CLI (current version 0.11.17): authorize a device from your browser, keep your own keys
4
4
  in an encrypted vault on this machine, hand an agent a TEE wallet it can trade, and manage API
5
5
  keys, wallets, and setup health from the terminal. Zero runtime dependencies; the whole thing is
6
6
  one self-contained `dist/index.js` that runs under plain Node.
@@ -80,6 +80,7 @@ emergency procedure, rebinding, trusted wallets and API key access levels are al
80
80
  | `candle wallets` (alias `candle wallet`) | Shows the account's embedded (launch) wallets and any linked wallets, using the API key, with a `Signer` column saying whether this machine holds each linked wallet's signing key and a `Trusted` column saying whether it is yours (linked while signed in, or marked with `trust`). It links and revokes; it never generates or prints a key, which the vault now derives. |
81
81
  | `candle wallets import --chain solana\|evm`, `wallets revoke <wallet-id>` | Link a wallet you own (key from `--key-file` or a hidden prompt, sealed to Privy before it leaves the machine), or unlink one. |
82
82
  | `candle wallets trust\|untrust <label\|address\|id\|prefix*>...` | Mark linked wallets as yours so an agent's Read:Write:Transfer key can move funds into them, or clear the mark. Owner only, over the device token, with a typed confirm; a trailing `*`, as in `'tr-*'`, selects by prefix. |
83
+ | `candle wallets allow-launch\|disallow-launch <label\|address\|id\|prefix*>...` | Let TEE wallets pay for a launch (`candle launch`), or stop them. Owner only, over the device token; the screen shows each wallet's label, address and bound key before a typed confirm. `disallow-launch --yes` skips the confirm. |
83
84
  | `candle wallets close-empty [--wallet embedded] [--keep <mint>]... [--yes]` | Closes the embedded wallet's empty token accounts and returns their rent to it, after a preview (`transfer:write`). |
84
85
  | `candle vault init\|status\|list\|new-key\|rename\|phrase show\|restore\|reconcile-exposure` | The encrypted local vault, `vault.enc` (Tier 1). `init` creates it with a passphrase factor and a 24-word recovery phrase (and no key), `new-key` derives a key inside it (`--count`, `--labels-from <file>` for many under one unlock), `list [filter] [--balances]` shows one line per key, `rename` changes a label only, `phrase show` displays the phrase on a terminal (never in `--json`, a log or a pipe), and `restore --phrase` rebuilds the derived keys on another machine with a new passphrase (`--count`, `--tee-count`, `--external-count`, `--evm-count` and `--evm-tee-count` for EVM keys and Hood TEE wallets, or a Solana gap scan with `--rpc-url`); a restored vault never allocates new keys. `reconcile-exposure` re-reads the account and adds exposure records, clearing nothing. No key ever leaves the vault to reach Candle. See [CLI custody](https://docs.candle.tv/developers/cli-custody). |
85
86
  | `candle vault factor list\|add passphrase\|add security-key\|add touch-id\|add passkey\|remove` (or `vault enroll <kind>`), `vault backup --to <path>\|icloud`, `vault verify-backup <path>` | What can open the vault, and proving a copy of it works: `backup` verifies the copy in full before reporting (a copy is sealed, keeping only the passphrase and security-key envelopes, unless it goes to a recognised local disk or removable drive, so a cloud folder and any path the CLI cannot place are both sealed unless `--accept-shared-domain` is passed), and `verify-backup` re-checks an existing one against this vault's key and address set. `add security-key` enrolls a FIDO2 security key (CTAP2 `hmac-secret`, user-verified with the key's PIN or biometric) through the bundled `candle-fido2` helper (`--install-helper` fetches and verifies this release's helper first); one key is not a recoverable factor, two keys are a pair, an enrolled key can authorize adding another, and the passphrase stays the recovery floor. `add touch-id` and `add passkey` are refused with `VAULT_FACTOR_UNSUPPORTED_ON_PLATFORM` until the Apple-signed helper ships. Every vault command then takes `--factor <id\|passphrase\|security-key>` to say which envelope opens it and `--device <id>` to name the key when several are attached. |
@@ -93,10 +94,10 @@ emergency procedure, rebinding, trusted wallets and API key access levels are al
93
94
  | `candle tee signer new --key <prefix\|label> [--out <pem>] [--force]` | On the trading machine, with that key's API key: generates the key's signer here and waits for the owner to approve it with `keys signer approve`. The key's wallets then trade from this machine. `--out` also writes a plaintext PEM for an SDK process. |
94
95
  | `candle tee rebind <wallet...> --to-key <prefix\|label>`, `candle tee rebinds [wallet]` | Move TEE wallets to another key on the same account (owner only, preview then `confirm`; funds and the relay signer do not move), and list the rebind history. |
95
96
  | `candle tee new\|enable` | The legacy path: `new` seals a fresh key in its own `tee-wallets.enc` and `enable <address> --vault <address>` delegates it with a pinned sweep vault. New setups use `vault promote`; `vault import-legacy --tee` migrates an existing store. |
96
- | `candle swap <from> <to> --amount <n>\|--percent <n> --wallet <tee> [--to <tee>] [--wait]` | Quote, confirm and swap through the TEE wallet's bound key; first buy after a launch is this command. 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. |
97
+ | `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. |
97
98
  | `candle swap status <id> [--kind trade\|swap\|launch] [--wait]` | Read an operation without resending it; `--wait` follows a bridge until it ends. |
98
99
  | `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 Read:Write:Transfer 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. |
99
- | `candle launch --name <name> --symbol <symbol> --image-url <url> --wallet <tee> [--quote-asset <asset>] [--dex-version v3\|v4]` | Create a token with no first buy; needs `launch:write`, operator-enabled `allowLaunch`, and the Pro or Max plan. 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. |
100
+ | `candle launch --name <name> --symbol <symbol> --image-url <url> --wallet <tee> [--buy <amount>] [--quote-asset <asset>] [--dex-version v3\|v4]` | Create a token from a TEE wallet. 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. |
100
101
  | `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. |
101
102
  | `candle portfolio [<filter>] [--chain solana\|hood] [--rpc-url <url>] [--evm-rpc-url <url>]` | 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. |
102
103
  | `candle plans` | The plans the API serves (`GET /api/v1/agent/plans`): each plan's monthly price, agent trade fee, Hyperliquid perps builder fee, the limits a new key gets, and one row per capability (buying and selling tokens not launched on Candle, self-launch, atomic launch, linked wallets, perps, limit orders). Every number is the server's; the CLI bundles none. Needs no key and sends none. `--json` is the server's body. Your own plan is `candle doctor`'s Plan row. |
@@ -408,13 +409,19 @@ Before signing, the command displays venue, price impact, tier fee, minimum rece
408
409
  all returned token warnings. Missing price impact is explicitly **unavailable**, never zero.
409
410
  `--yes` skips the ordinary prompt but still prints warnings. With `--json`, stdout is one
410
411
  JSON result containing the quote; the preview and operation id go to stderr. Launch creation
411
- has no price impact or token receipt, shows its maximum SOL debit, and makes no first buy.
412
- Use a separate swap for that buy.
413
-
414
- Swaps require `swap:write`. Launch requires `launch:write` **and** operator-enabled
415
- `allowLaunch` on the wallet. The operator sets or clears it through the shipped
416
- `PUT /api/v1/agent/wallets/<id>/capabilities` endpoint with device/session authentication and
417
- `{"capability":"allowLaunch","enabled":true}`. An agent key cannot grant itself this permission.
412
+ has no price impact or token receipt and shows its maximum SOL debit. Without `--buy` it makes
413
+ no first buy; use a separate swap for that buy. With `--buy <amount>` (whole units of the quote
414
+ asset, as `--amount` is for a swap) the same wallet buys at launch: in the launch transaction on
415
+ Solana, as a last leg after the curve and the fee on Hood. Candle reserves the buy against the
416
+ key's spend window before anything is signed. `--buy` needs the server's
417
+ `TEE_LAUNCH_BUYS_ENABLED=1`, which is off by default; while it is off Candle refuses the buy
418
+ (`VALIDATION_FAILED` on `buyAmount`) and nothing is built or signed.
419
+
420
+ Swaps require `swap:write`. Launch requires `launch:write` **and** `allowLaunch` on the
421
+ wallet, which is off by default. The account owner (not Candle, and not the agent) sets it with
422
+ `candle wallets allow-launch <wallet>` and clears it with `candle wallets disallow-launch <wallet>`,
423
+ over the device token from `candle auth login` (the same endpoint, `PUT /api/v1/agent/wallets/<id>/capabilities`,
424
+ takes a device token or a web session). An agent key cannot grant itself this permission.
418
425
 
419
426
  For scripts, always supply a stable `--client-trade-id` for each intention. Otherwise the CLI
420
427
  generates and prints one. Reusing an existing id reads its status instead of rebuilding.
package/dist/index.js CHANGED
@@ -14506,8 +14506,29 @@ async function request(ctx, key, path, body) {
14506
14506
  fetch: ctx.deps.fetch,
14507
14507
  env: ctx.deps.env
14508
14508
  });
14509
- if (!result.ok)
14510
- throw new TradingError(result.code ?? "REQUEST_FAILED", result.message);
14509
+ if (!result.ok) {
14510
+ const raw = result.raw;
14511
+ const error = raw?.error;
14512
+ const details = {};
14513
+ for (const field of [
14514
+ "field",
14515
+ "stage",
14516
+ "signature",
14517
+ "leg",
14518
+ "legs",
14519
+ "hashes",
14520
+ "retryable",
14521
+ "evidenceStored",
14522
+ "terminal"
14523
+ ]) {
14524
+ if (error?.[field] !== undefined && (field === "field" || path === "/api/v1/agent/swap/submit"))
14525
+ details[field] = error[field];
14526
+ }
14527
+ throw new TradingError(result.code ?? "REQUEST_FAILED", result.message, {
14528
+ ...Object.keys(details).length ? { details } : {},
14529
+ ...path === "/api/v1/agent/swap/submit" && error?.stage === "unconfirmed" ? { exitCode: 3 } : {}
14530
+ });
14531
+ }
14511
14532
  if (!result.body || typeof result.body !== "object")
14512
14533
  throw new TradingError("INVALID_RESPONSE", "Candle returned an invalid response.");
14513
14534
  return result.body;
@@ -14553,7 +14574,7 @@ async function completeTradingWallet(ctx, row, appId, scope, chain2 = "solana",
14553
14574
  if (!row.active)
14554
14575
  throw new TradingError("TEE_WALLET_INACTIVE", `The payer must be a verified-active ${chainName(chain2)} TEE wallet.`);
14555
14576
  if (scope === "launch:write" && row.allowLaunch !== true)
14556
- throw new TradingError("LAUNCH_NOT_ALLOWED", `The operator must enable allowLaunch for wallet ${row.id} through PUT /api/v1/agent/wallets/${row.id}/capabilities using device/session authentication.`);
14577
+ throw new TradingError("LAUNCH_NOT_ALLOWED", `The account owner must turn on allowLaunch for wallet ${row.id}: run candle wallets allow-launch ${row.id} on a profile signed in with candle auth login. An agent key cannot.`);
14557
14578
  if (!appId || !row.privyWalletId)
14558
14579
  throw new TradingError("SIGNER_UNAVAILABLE", "The server did not return its relay public identifiers.");
14559
14580
  const signer = await localSignerFor(ctx.deps, row.id, row.signerQuorumId);
@@ -30311,6 +30332,8 @@ function tradingFailure(ctx, error, id) {
30311
30332
  writeUsageFailure(ctx.deps, error.message, ctx.json);
30312
30333
  return 2;
30313
30334
  }
30335
+ if (error instanceof TradingError && error.details)
30336
+ describeSwapEvidence(ctx, error.details, error.details.terminal === true);
30314
30337
  writeLocalFailure(ctx.deps, {
30315
30338
  code: error instanceof TradingError ? error.code : "TRADING_FAILED",
30316
30339
  message: `${error instanceof Error ? describeRpcFailure(error) : "Trading failed."}${id ? ` Operation ${id}; use candle swap status ${id} before another attempt.` : ""}`,
@@ -30335,7 +30358,31 @@ async function tradingRead(ctx, client, read) {
30335
30358
  throw error;
30336
30359
  }
30337
30360
  }
30361
+ async function requestSwapBuild(ctx, key, body) {
30362
+ try {
30363
+ return await request(ctx, key, "/api/v1/agent/swap/build", body);
30364
+ } catch (error) {
30365
+ if (body.payer?.type === "main" && error instanceof TradingError && error.code === "VALIDATION_FAILED" && error.details?.field === "payer") {
30366
+ throw new TradingError("EMBEDDED_PAYER_UNSUPPORTED", "This Candle deployment cannot hold an embedded-wallet swap back for confirmation. Nothing was built. Name a TEE wallet with --wallet, or point at a deployment that supports embedded swap builds.");
30367
+ }
30368
+ throw error;
30369
+ }
30370
+ }
30371
+ function describeSwapEvidence(ctx, details, terminal) {
30372
+ if (details.stage === "not_broadcast" && terminal)
30373
+ ctx.deps.stderr.write(`Nothing was sent. This swap job is terminal; retry with a new id.
30374
+ `);
30375
+ if (details.stage === "unconfirmed") {
30376
+ const hashes = Array.isArray(details.hashes) ? details.hashes : typeof details.signature === "string" ? [details.signature] : [];
30377
+ ctx.deps.stderr.write(`Swap outcome needs verification${details.leg !== undefined ? ` (leg ${safeText(String(details.leg))})` : ""}. Known hashes: ${hashes.length ? hashes.map((hash) => safeText(String(hash))).join(", ") : "unknown"}. Do not automatically retry or use a new id.
30378
+ `);
30379
+ }
30380
+ }
30338
30381
  function printTradingResult(ctx, result) {
30382
+ if (result.kind === "swap" && result.job && typeof result.job === "object") {
30383
+ const job = result.job;
30384
+ describeSwapEvidence(ctx, job, job.status === "failed");
30385
+ }
30339
30386
  ctx.deps.stdout.write(ctx.json ? `${JSON.stringify(result)}
30340
30387
  ` : `${JSON.stringify(result, null, 2)}
30341
30388
  `);
@@ -30533,8 +30580,6 @@ async function swap(args, ctx) {
30533
30580
  if (prior)
30534
30581
  return printTradingResult(ctx, prior);
30535
30582
  const payerWallet = await tradingPayer(ctx, key, flags["--wallet"]);
30536
- if (payerWallet.kind === "embedded" && kind === "swap")
30537
- throw new TradingError("PAIR_UNSUPPORTED", "The embedded wallet cannot swap between base assets from this command yet: that rail executes in one call, so there would be nothing to confirm. Trade a token with it, or name a TEE wallet for a base pair.");
30538
30583
  const wallet = payerWallet.kind === "tee" ? payerWallet.wallet : { address: payerWallet.address };
30539
30584
  const solana = lazySolanaClient(ctx, flags["--rpc-url"]);
30540
30585
  const decimals = await decimalsFor(ctx, from, solana);
@@ -30560,14 +30605,14 @@ async function swap(args, ctx) {
30560
30605
  amountRaw = rawAmount(flags["--amount"], decimals);
30561
30606
  if (BigInt(amountRaw) > BigInt(Number.MAX_SAFE_INTEGER))
30562
30607
  throw new TradingError("INVALID_AMOUNT", "Amount exceeds the venue's exact integer range.");
30563
- if (payerWallet.kind === "embedded")
30608
+ if (payerWallet.kind === "embedded" && kind === "trade")
30564
30609
  await assertDeferredExecuteSupported(ctx, key, id);
30565
30610
  if (!await claimOperation(ctx, key, id, kind))
30566
30611
  throw new TradingError("OPERATION_ALREADY_STARTED", "This machine already started this id; no write was resent.");
30567
30612
  ctx.deps.stderr.write(`Operation: ${id}
30568
30613
  `);
30569
30614
  const payer = payerWallet.kind === "tee" ? { type: "linked", linkedWalletId: payerWallet.wallet.id } : { type: "main" };
30570
- const built = kind === "swap" ? await request(ctx, key, "/api/v1/agent/swap/build", {
30615
+ const built = kind === "swap" ? await requestSwapBuild(ctx, key, {
30571
30616
  clientTradeId: id,
30572
30617
  from,
30573
30618
  to,
@@ -30589,7 +30634,7 @@ async function swap(args, ctx) {
30589
30634
  return printTradingResult(ctx, { ...built, clientTradeId: id, kind });
30590
30635
  const data = swapBuildSchema.parse(kind === "swap" ? built.payload : built);
30591
30636
  if (kind === "swap" && (data.venue !== "jupiter" || data.recipient !== wallet.address))
30592
- throw new TradingError("INVALID_RESPONSE", "A TEE base swap must use Jupiter and return to its payer.");
30637
+ throw new TradingError("INVALID_RESPONSE", "A base swap must use Jupiter and return to its payer.");
30593
30638
  if (kind === "trade" && (built.chain !== "solana" || built.walletAddress !== wallet.address))
30594
30639
  throw new TradingError("INVALID_RESPONSE", "The token build does not name the requested Solana payer.");
30595
30640
  const artifacts = kind === "swap" ? { ...data, transactionBase64: undefined, quoteSource: undefined, quoteAsset: undefined } : data.artifacts;
@@ -30616,9 +30661,11 @@ async function swap(args, ctx) {
30616
30661
  if (!Number.isFinite(data.expiresAt) || data.expiresAt <= ctx.deps.now())
30617
30662
  throw new TradingError("QUOTE_EXPIRED", "The quote expired before signing. Start a new intention with a new id.");
30618
30663
  if (payerWallet.kind === "embedded") {
30619
- const executed = await request(ctx, key, "/api/v1/trade/agent/execute", { clientTradeId: id });
30664
+ const executed = kind === "swap" ? await request(ctx, key, "/api/v1/agent/swap/submit", { clientTradeId: id, swapId: data.swapId }) : await request(ctx, key, "/api/v1/trade/agent/execute", { clientTradeId: id });
30665
+ const settled2 = kind === "swap" ? reportSettlement(ctx, executed.payload?.settlement, { asset: to, decimals: outDecimals }, id) : {};
30620
30666
  return printTradingResult(ctx, {
30621
30667
  ...executed,
30668
+ ...settled2,
30622
30669
  clientTradeId: id,
30623
30670
  kind,
30624
30671
  quote,
@@ -30728,8 +30775,6 @@ async function hoodSwap(ctx, args) {
30728
30775
  if (prior)
30729
30776
  return printTradingResult(ctx, prior);
30730
30777
  const payer = await tradingPayer(ctx, key, flags["--wallet"], "swap:write", "hood");
30731
- if (payer.kind === "embedded" && kind === "swap")
30732
- throw new TradingError("PAIR_UNSUPPORTED", "The embedded wallet cannot swap ETH and USDG from this command: that rail executes in one call, so there would be nothing to confirm. Trade a token with it, or name a Hood TEE wallet.");
30733
30778
  const payerAddress = payer.kind === "tee" ? payer.wallet.address : payer.address;
30734
30779
  const rpc = lazyEvmRpc(ctx, flags["--rpc-url"]);
30735
30780
  const decimals = await hoodDecimals(from, rpc);
@@ -30742,7 +30787,7 @@ async function hoodSwap(ctx, args) {
30742
30787
  throw new TradingError("INVALID_AMOUNT", "The selected percentage rounds to zero raw units.");
30743
30788
  } else
30744
30789
  amountRaw = rawAmount(flags["--amount"], decimals);
30745
- if (payer.kind === "embedded")
30790
+ if (payer.kind === "embedded" && kind === "trade")
30746
30791
  await assertDeferredExecuteSupported(ctx, key, id);
30747
30792
  if (!await claimOperation(ctx, key, id, kind))
30748
30793
  throw new TradingError("OPERATION_ALREADY_STARTED", "This machine already started this id; no write was resent.");
@@ -30751,7 +30796,7 @@ async function hoodSwap(ctx, args) {
30751
30796
  const payerBody = payer.kind === "tee" ? { type: "linked", linkedWalletId: payer.wallet.id } : { type: "main" };
30752
30797
  const base = from.base ?? to.base;
30753
30798
  const token = from.base ? to.asset : from.asset;
30754
- const built = kind === "swap" ? await request(ctx, key, "/api/v1/agent/swap/build", {
30799
+ const built = kind === "swap" ? await requestSwapBuild(ctx, key, {
30755
30800
  clientTradeId: id,
30756
30801
  from: from.asset,
30757
30802
  to: to.asset,
@@ -30825,8 +30870,16 @@ async function hoodSwap(ctx, args) {
30825
30870
  if (!Number.isFinite(data.expiresAt) || data.expiresAt <= ctx.deps.now())
30826
30871
  throw new TradingError("QUOTE_EXPIRED", "The quote expired before signing. Start a new intention with a new id.");
30827
30872
  if (payer.kind === "embedded") {
30828
- const executed = await request(ctx, key, "/api/v1/trade/agent/execute", { clientTradeId: id });
30829
- return printTradingResult(ctx, { ...executed, clientTradeId: id, kind, quote, wallet: safeText(payerAddress) });
30873
+ const executed = kind === "swap" ? await request(ctx, key, "/api/v1/agent/swap/submit", { clientTradeId: id, swapId: data.swapId }) : await request(ctx, key, "/api/v1/trade/agent/execute", { clientTradeId: id });
30874
+ const settled2 = kind === "swap" ? reportSettlement(ctx, executed.payload?.settlement, { asset: to.asset, decimals: outDecimals }, id) : {};
30875
+ return printTradingResult(ctx, {
30876
+ ...executed,
30877
+ ...settled2,
30878
+ clientTradeId: id,
30879
+ kind,
30880
+ quote,
30881
+ wallet: safeText(payerAddress)
30882
+ });
30830
30883
  }
30831
30884
  const wallet = payer.wallet;
30832
30885
  if (wallet.chain !== "evm" || !sequenced)
@@ -30935,7 +30988,7 @@ async function bridgeSwap(ctx, args) {
30935
30988
  throw new TradingError("OPERATION_ALREADY_STARTED", "This machine already started this id; no write was resent.");
30936
30989
  ctx.deps.stderr.write(`Operation: ${id}
30937
30990
  `);
30938
- const built = await request(ctx, key, "/api/v1/agent/swap/build", {
30991
+ const built = await requestSwapBuild(ctx, key, {
30939
30992
  clientTradeId: id,
30940
30993
  from,
30941
30994
  to,
@@ -50758,7 +50811,7 @@ var init_convert = __esm(() => {
50758
50811
  });
50759
50812
 
50760
50813
  // ../mcp/src/version.ts
50761
- var SERVER_VERSION = "0.10.6";
50814
+ var SERVER_VERSION = "0.10.7";
50762
50815
 
50763
50816
  // ../mcp/src/update-notice.ts
50764
50817
  function newer(a, b) {
@@ -52063,7 +52116,7 @@ function planTableMarkdown(table) {
52063
52116
  return [line(headers2), line(headers2.map(() => "---")), ...rows.map(line)].join(`
52064
52117
  `);
52065
52118
  }
52066
- var PLAN_CAPABILITY_NOTE2 = "A capability marked yes is what the plan allows. It is subject to the deployment's own switches (perps and own-wallet bridges each have one) and to the wallet, scopes and setup the feature needs.", PLAN_CAPABILITY_LABELS2, PLAN_LABELS2;
52119
+ var PLAN_CAPABILITY_NOTE2 = "A capability marked yes is what the plan allows. It is subject to the deployment's own switches (perps and own-wallet bridges each have one) and to the wallet, scopes and setup the feature needs. Every plan can launch a token from its embedded wallet, with an optional dev buy (same transaction on Solana; best-effort follow-up on Hood), through the headless launch; the two launch rows are additional routes, not the only ones.", PLAN_CAPABILITY_LABELS2, PLAN_LABELS2;
52067
52120
  var init_plans = __esm(() => {
52068
52121
  PLAN_CAPABILITY_LABELS2 = {
52069
52122
  tradeCandleTokens: "Trade Candle-launched tokens",
@@ -52072,8 +52125,8 @@ var init_plans = __esm(() => {
52072
52125
  sellExternalTokens: "Sell tokens not launched on Candle",
52073
52126
  buyExternalTokens: "Buy tokens not launched on Candle",
52074
52127
  hyperliquidPerps: "Hyperliquid perps (when enabled)",
52075
- selfLaunch: "Self-launch from a linked wallet",
52076
- atomicLaunch: "Atomic launch with first buys",
52128
+ selfLaunch: "Launch from a linked or TEE wallet (self-signed)",
52129
+ atomicLaunch: "Atomic launch: launch + 1–4 first buys in one bundle",
52077
52130
  createLinkedWallets: "Create linked wallets",
52078
52131
  importLinkedWallets: "Import linked wallets",
52079
52132
  limitOrders: "Limit orders",
@@ -52288,7 +52341,7 @@ function registerTools(server, env = process.env) {
52288
52341
  };
52289
52342
  register("candle_launch_token", {
52290
52343
  title: "Launch a token on Candle",
52291
- description: "Launch a new token via the Candle headless launch API. Set dryRun: true to validate without spending anything.",
52344
+ description: "Launch a new token via the Candle headless launch API, from the account's embedded wallet. Works on every plan, Free included. Set dryRun: true to validate without spending anything.",
52292
52345
  inputSchema: launchTokenShape
52293
52346
  }, async (args) => callAndRelay("candle_launch_token", args, cfg));
52294
52347
  register("candle_get_market", {
@@ -52454,7 +52507,7 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
52454
52507
  });
52455
52508
  register("candle_launch_and_seed", {
52456
52509
  title: "Launch a token and seed it",
52457
- description: "Launch a new token with an optional dev-buy seed bundled into the launch itself, 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.",
52510
+ 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.",
52458
52511
  inputSchema: launchAndSeedShape
52459
52512
  }, async (args) => {
52460
52513
  const result = await executeLaunchAndSeed(args, cfg, fetch);
@@ -52631,7 +52684,7 @@ var init_tools = __esm(() => {
52631
52684
  launchAndSeedShape = {
52632
52685
  ...seedableLaunchShape,
52633
52686
  clientLaunchId: exports_external.string().optional().describe("Idempotency key. Auto-generated when omitted and echoed in the result."),
52634
- 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. 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.")
52687
+ 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.")
52635
52688
  };
52636
52689
  transferShape = {
52637
52690
  chain: exports_external.enum(["solana", "hood"]).describe("Which chain the transfer executes on"),
@@ -52770,7 +52823,7 @@ init_render();
52770
52823
  init_secret_store();
52771
52824
 
52772
52825
  // src/version.ts
52773
- var CLI_VERSION = "0.11.16";
52826
+ var CLI_VERSION = "0.11.17";
52774
52827
 
52775
52828
  // src/commands/auth.ts
52776
52829
  init_keys_embedded_wallet();
@@ -53282,7 +53335,7 @@ var HELP = {
53282
53335
  swap: {
53283
53336
  group: "Trade",
53284
53337
  summary: "Quote, confirm and swap on Solana or Hood, bridge between them; read an operation by id",
53285
- description: "Swaps run through a TEE wallet's bound key: the quote is shown and confirmed before anything is sent, and the first buy after a launch is this command rather than part of the launch. The assets decide the chain (ETH, USDG or a 0x token is Hood; SOL, USDC, CNDL or a mint is Solana) and a named wallet must be on it. A Hood TEE wallet signs one leg at a time: approve, Permit2, trade, then the fee, each only after the one before it landed. SOL or USDC to ETH or USDG, or back, is a bridge through Relay from a TEE wallet into this key's own TEE wallet on the other chain (--to names it when there are several); Candle charges no fee, the key needs a raw cap on the origin asset, and this machine checks Relay's deposit before it is signed. The bridge prints its deposit and returns while Relay fills; --wait follows it for up to ten minutes. While it is open, a sweep of either wallet refuses BRIDGE_IN_FLIGHT for two hours. Vault to vault: fund the Solana TEE wallet from the vault, bridge, then disable and sweep the Hood TEE wallet into its vault. Every plan can trade Candle-launched tokens and base assets, and sell a token it holds that was not launched on Candle; buying such a token needs Pro or Max (TIER_REQUIRED otherwise). Each trade pays the plan's agent fee on top; a base-pair swap or a bridge between this key's own wallets pays none. candle plans shows the fees in force.",
53338
+ description: "Swaps run through a TEE wallet's bound key: the quote is shown and confirmed before anything is sent. The first buy after a launch is this command, unless the launch carried it with candle launch --buy. The assets decide the chain (ETH, USDG or a 0x token is Hood; SOL, USDC, CNDL or a mint is Solana) and a named wallet must be on it. A Hood TEE wallet signs one leg at a time: approve, Permit2, trade, then the fee, each only after the one before it landed. SOL or USDC to ETH or USDG, or back, is a bridge through Relay from a TEE wallet into this key's own TEE wallet on the other chain (--to names it when there are several); Candle charges no fee, the key needs a raw cap on the origin asset, and this machine checks Relay's deposit before it is signed. The bridge prints its deposit and returns while Relay fills; --wait follows it for up to ten minutes. While it is open, a sweep of either wallet refuses BRIDGE_IN_FLIGHT for two hours. Vault to vault: fund the Solana TEE wallet from the vault, bridge, then disable and sweep the Hood TEE wallet into its vault. Every plan can trade Candle-launched tokens and base assets, and sell a token it holds that was not launched on Candle; buying such a token needs Pro or Max (TIER_REQUIRED otherwise). Each trade pays the plan's agent fee on top; a base-pair swap or a bridge between this key's own wallets pays none. candle plans shows the fees in force.",
53286
53339
  usage: ["candle swap <from> <to> [flags]", "candle swap status <id>"],
53287
53340
  rows: [
53288
53341
  {
@@ -53440,10 +53493,10 @@ var HELP = {
53440
53493
  },
53441
53494
  launch: {
53442
53495
  group: "Trade",
53443
- summary: "Create a token on Solana or Hood (the first buy is a separate swap)",
53444
- description: "Creates a token with no first buy, so the launch and the position are two decisions rather than one. Needs the launch:write scope and an operator-enabled allowLaunch. 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), each only after the one before it landed. Launching from a TEE wallet needs Pro or Max (TIER_REQUIRED otherwise; candle plans).",
53496
+ summary: "Create a token on Solana or Hood, with an optional first buy",
53497
+ 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 rides in the launch transaction, and on Hood it is a last leg after the curve and the fee. 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.",
53445
53498
  usage: [
53446
- "candle launch --name <name> --symbol <symbol> --image-url <url> --wallet <tee> [--quote-asset <asset>] [--dex-version v3|v4]"
53499
+ "candle launch --name <name> --symbol <symbol> --image-url <url> --wallet <tee> [--buy <amount>] [--quote-asset <asset>] [--dex-version v3|v4]"
53447
53500
  ],
53448
53501
  rows: [],
53449
53502
  flags: [
@@ -53451,6 +53504,10 @@ var HELP = {
53451
53504
  { invocation: "--symbol <symbol>", description: "The token's ticker" },
53452
53505
  { invocation: "--image-url <url>", description: "The token image, already hosted" },
53453
53506
  { invocation: "--wallet <tee>", description: "The TEE wallet that creates it" },
53507
+ {
53508
+ invocation: "--buy <amount>",
53509
+ description: "A first buy from the same wallet, in whole units of the quote asset; needs TEE_LAUNCH_BUYS_ENABLED on the server"
53510
+ },
53454
53511
  { invocation: "--quote-asset <asset>", description: "sol, usdc or cndl on Solana; eth or usdg on Hood" },
53455
53512
  {
53456
53513
  invocation: "--dex-version v3|v4",
@@ -53459,7 +53516,8 @@ var HELP = {
53459
53516
  ],
53460
53517
  examples: [
53461
53518
  "candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet AgentOne",
53462
- "candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet HoodAgent --dex-version v4"
53519
+ "candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet HoodAgent --dex-version v4",
53520
+ "candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet AgentOne --buy 0.1"
53463
53521
  ],
53464
53522
  env: ENV_API
53465
53523
  },
@@ -53596,6 +53654,14 @@ var HELP = {
53596
53654
  invocation: "untrust <label|address|id|prefix*>... [--yes]",
53597
53655
  description: "Clear the mark; moving funds in needs the withdrawal allowlist again (owner only)"
53598
53656
  },
53657
+ {
53658
+ invocation: "allow-launch <label|address|id|prefix*>...",
53659
+ description: "Let TEE wallets pay for a launch (candle launch), after a screen of label, address and bound key (owner only; typed confirm)"
53660
+ },
53661
+ {
53662
+ invocation: "disallow-launch <label|address|id|prefix*>... [--yes]",
53663
+ description: "Stop TEE wallets paying for a launch; trading is unchanged (owner only)"
53664
+ },
53599
53665
  {
53600
53666
  invocation: "close-empty [--wallet embedded] [--keep <mint>]... [--client-trade-id <id>] [--yes]",
53601
53667
  description: "Close the embedded wallet's empty token accounts and return their rent to it, after a preview (transfer:write)"
@@ -53606,6 +53672,7 @@ var HELP = {
53606
53672
  "candle wallet import --chain solana --key-file ./signer.json",
53607
53673
  "candle wallet revoke wal_123",
53608
53674
  "candle wallet trust 'tr-*' 'dest-*'",
53675
+ "candle wallet allow-launch launcher-1",
53609
53676
  "candle wallet close-empty --keep EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
53610
53677
  ],
53611
53678
  env: ENV_API
@@ -59851,7 +59918,7 @@ init_solana_lite();
59851
59918
  init_trading();
59852
59919
  init_swap();
59853
59920
  import { randomUUID as randomUUID2 } from "node:crypto";
59854
- var USAGE2 = "Usage: candle launch --name <name> --symbol <symbol> --image-url <https-url> --wallet <tee> [--client-trade-id <id>] [--quote-asset sol|usdc|cndl|eth|usdg] [--dex-version v3|v4] [--mode <mode>] [--rpc-url <url>] [--yes]. A Hood TEE wallet launches on Hood and needs --dex-version.";
59921
+ var USAGE2 = "Usage: candle launch --name <name> --symbol <symbol> --image-url <https-url> --wallet <tee> [--buy <amount>] [--client-trade-id <id>] [--quote-asset sol|usdc|cndl|eth|usdg] [--dex-version v3|v4] [--mode <mode>] [--rpc-url <url>] [--yes]. A Hood TEE wallet launches on Hood and needs --dex-version.";
59855
59922
  var QUOTE_CHAIN = {
59856
59923
  sol: "solana",
59857
59924
  usdc: "solana",
@@ -59860,6 +59927,34 @@ var QUOTE_CHAIN = {
59860
59927
  usdg: "hood"
59861
59928
  };
59862
59929
  var HOOD_LAUNCH_LEGS = ["createCurve", "feeTransfer"];
59930
+ var HOOD_LAUNCH_BUY_LEGS = ["createCurve", "feeTransfer", "trade"];
59931
+ function launchBuy(flags, chain2) {
59932
+ const amount = flags["--buy"];
59933
+ if (amount === undefined)
59934
+ return;
59935
+ const quote = (flags["--quote-asset"] ?? (chain2 === "hood" ? "eth" : "sol")).toUpperCase();
59936
+ const decimals = chain2 === "hood" ? HOOD_BASES[quote]?.decimals : BASES[quote]?.decimals;
59937
+ if (decimals === undefined)
59938
+ throw new TradingError("INVALID_AMOUNT", `--buy is in the launch's quote asset, and ${safeText(quote)} is not one on ${chainName(chain2)}. Nothing was built.`);
59939
+ const raw = rawAmount(amount, decimals);
59940
+ return { raw, display: `${decimalAmount(raw, decimals)} ${quote}` };
59941
+ }
59942
+ function buyRefusal(error) {
59943
+ if (!(error instanceof TradingError) || error.code !== "VALIDATION_FAILED" || error.details?.field !== "buyAmount")
59944
+ return error;
59945
+ return new TradingError("VALIDATION_FAILED", `Candle refused the first buy on this TEE launch: ${safeText(error.message)}. A TEE wallet's launch takes --buy only where the server has TEE_LAUNCH_BUYS_ENABLED on, and it is off on this deployment. Nothing was built or signed.`, {
59946
+ suggestion: "Launch without --buy under a new --client-trade-id, then make the first buy with candle swap once the token lands.",
59947
+ details: error.details
59948
+ });
59949
+ }
59950
+ function buyNotice(buy, chain2) {
59951
+ if (!buy)
59952
+ return `Launch creates the token only. Make the first buy with a separate candle swap. Price impact does not apply to creation.
59953
+ `;
59954
+ return chain2 === "hood" ? `Launch creates the token, then spends ${buy.display} on a first buy on the new curve, as the last leg. Candle reserves the buy against this key's spend window before any leg is signed. If the buy leg never lands, the token exists with no first buy. Price impact does not apply to creation.
59955
+ ` : `Launch creates the token and spends ${buy.display} on a first buy in the same transaction. Candle reserves the whole debit against this key's spend window before anything is signed. Price impact does not apply to creation.
59956
+ `;
59957
+ }
59863
59958
  function flagChain(flags) {
59864
59959
  const said = [];
59865
59960
  const named = walletNameChain(flags["--wallet"] ?? "");
@@ -59889,7 +59984,8 @@ async function launch(args, ctx) {
59889
59984
  "--rpc-url",
59890
59985
  "--quote-asset",
59891
59986
  "--mode",
59892
- "--dex-version"
59987
+ "--dex-version",
59988
+ "--buy"
59893
59989
  ],
59894
59990
  booleanFlags: ["--yes"]
59895
59991
  });
@@ -59904,6 +60000,8 @@ async function launch(args, ctx) {
59904
60000
  return 2;
59905
60001
  }
59906
60002
  try {
60003
+ if (flags["--buy"] !== undefined)
60004
+ rawAmount(flags["--buy"], 18);
59907
60005
  const hinted = flagChain(flags);
59908
60006
  if (hinted === "hood" && flags["--dex-version"] === undefined) {
59909
60007
  writeUsageFailure(ctx.deps, `A Hood launch needs --dex-version v3|v4. ${USAGE2}`, ctx.json);
@@ -59929,8 +60027,10 @@ async function launch(args, ctx) {
59929
60027
  if (wallet.chain === "evm") {
59930
60028
  if (flags["--dex-version"] === undefined)
59931
60029
  throw new TradingUsage(`${wallet.address} is a Hood TEE wallet, and a Hood launch needs --dex-version v3|v4.`);
59932
- return await hoodLaunch(ctx, key, { flags, id, wallet, yes: parsed.booleans.has("--yes") });
60030
+ const buy2 = launchBuy(flags, "hood");
60031
+ return await hoodLaunch(ctx, key, { flags, id, wallet, buy: buy2, yes: parsed.booleans.has("--yes") });
59933
60032
  }
60033
+ const buy = launchBuy(flags, "solana");
59934
60034
  const solana = await tradingSolanaClient(ctx, flags["--rpc-url"]);
59935
60035
  if (!await claimOperation(ctx, key, id, "launch"))
59936
60036
  throw new TradingError("OPERATION_ALREADY_STARTED", "This machine already started this launch id; no write was resent.");
@@ -59939,7 +60039,7 @@ async function launch(args, ctx) {
59939
60039
  const built = launchBuildSchema.parse(await request(ctx, key, "/api/v1/launch/self/build", {
59940
60040
  clientLaunchId: id,
59941
60041
  chain: "solana",
59942
- buyAmount: 0,
60042
+ buyAmount: buy ? buy.raw : 0,
59943
60043
  name: flags["--name"],
59944
60044
  symbol: flags["--symbol"],
59945
60045
  imageUrl: flags["--image-url"],
@@ -59947,21 +60047,22 @@ async function launch(args, ctx) {
59947
60047
  ...flags["--description"] ? { description: flags["--description"] } : {},
59948
60048
  ...flags["--quote-asset"] ? { quoteAsset: flags["--quote-asset"] } : {},
59949
60049
  ...flags["--mode"] ? { mode: flags["--mode"] } : {}
60050
+ }).catch((error) => {
60051
+ throw buyRefusal(error);
59950
60052
  }));
59951
60053
  if (!built.transaction || !/^\d+$/.test(built.maxDebitLamports ?? ""))
59952
60054
  throw new TradingError("INVALID_RESPONSE", "Candle did not return a launch transaction and maximum debit; nothing was signed.");
59953
60055
  const quote = {
59954
- intent: `Launch ${flags["--name"]} (${flags["--symbol"]})`,
60056
+ intent: `Launch ${flags["--name"]} (${flags["--symbol"]})${buy ? ` with a first buy of ${buy.display}` : ""}`,
59955
60057
  wallet: wallet.address,
59956
60058
  venue: "curve launch",
59957
60059
  priceImpactPct: null,
59958
60060
  fee: built.fee ?? { bps: 0, feeRaw: "0" },
59959
- minimumReceived: "0 tokens (no first buy)",
60061
+ minimumReceived: buy ? "not quoted (the first buy on the new curve)" : "0 tokens (no first buy)",
59960
60062
  maxDebitLamports: built.maxDebitLamports,
59961
60063
  tokenRisks: []
59962
60064
  };
59963
- ctx.deps.stderr.write(`Launch creates the token only. Make the first buy with a separate candle swap. Price impact does not apply to creation.
59964
- `);
60065
+ ctx.deps.stderr.write(buyNotice(buy, "solana"));
59965
60066
  if (!await confirmQuote(ctx, quote, parsed.booleans.has("--yes")))
59966
60067
  return printTradingResult(ctx, { success: true, status: "cancelled", clientTradeId: id, kind: "launch", quote });
59967
60068
  if (!Number.isFinite(built.expiresAt) || built.expiresAt <= ctx.deps.now())
@@ -59987,7 +60088,8 @@ async function launch(args, ctx) {
59987
60088
  }
59988
60089
  }
59989
60090
  async function hoodLaunch(ctx, key, args) {
59990
- const { flags, id, wallet } = args;
60091
+ const { flags, id, wallet, buy } = args;
60092
+ const allowedLegs = buy ? HOOD_LAUNCH_BUY_LEGS : HOOD_LAUNCH_LEGS;
59991
60093
  if (!await claimOperation(ctx, key, id, "launch"))
59992
60094
  throw new TradingError("OPERATION_ALREADY_STARTED", "This machine already started this launch id; no write was resent.");
59993
60095
  ctx.deps.stderr.write(`Operation: ${id}
@@ -59995,7 +60097,7 @@ async function hoodLaunch(ctx, key, args) {
59995
60097
  const built = await request(ctx, key, "/api/v1/launch/self/build", {
59996
60098
  clientLaunchId: id,
59997
60099
  chain: "hood",
59998
- buyAmount: 0,
60100
+ buyAmount: buy ? buy.raw : 0,
59999
60101
  name: flags["--name"],
60000
60102
  symbol: flags["--symbol"],
60001
60103
  imageUrl: flags["--image-url"],
@@ -60004,6 +60106,8 @@ async function hoodLaunch(ctx, key, args) {
60004
60106
  ...flags["--description"] ? { description: flags["--description"] } : {},
60005
60107
  ...flags["--quote-asset"] ? { quoteAsset: flags["--quote-asset"].toLowerCase() } : {},
60006
60108
  ...flags["--mode"] ? { mode: flags["--mode"] } : {}
60109
+ }).catch((error) => {
60110
+ throw buyRefusal(error);
60007
60111
  });
60008
60112
  const parsed = sequencedSchema.safeParse(built);
60009
60113
  if (!parsed.success)
@@ -60012,26 +60116,25 @@ async function hoodLaunch(ctx, key, args) {
60012
60116
  const curve = typeof built.curveAddress === "string" ? checkEvmAddress(built.curveAddress) : undefined;
60013
60117
  if (built.chain !== "hood" || built.clientLaunchId !== id || typeof built.walletAddress !== "string" || !sameEvmAddress(built.walletAddress, wallet.address) || !curve?.ok)
60014
60118
  throw new TradingError("INVALID_RESPONSE", "The Hood launch build does not name this launch and payer; nothing was signed.");
60015
- if (first.legKind !== "createCurve" || first.plannedLegCount > HOOD_LAUNCH_LEGS.length)
60119
+ if (first.legKind !== "createCurve" || first.plannedLegCount > allowedLegs.length || buy !== undefined && first.plannedLegCount < 2)
60016
60120
  throw new TradingError("INVALID_RESPONSE", `A Hood launch starts with its createCurve leg, and Candle offered ${safeText(first.legKind)} of ${first.plannedLegCount}; nothing was signed.`);
60017
60121
  const leg = first.nextLeg;
60018
60122
  const maxFee = BigInt(leg.maxFeePerGas);
60019
60123
  const reserve = sweepReserveFloor(maxFee);
60020
60124
  const quote = {
60021
- intent: `Launch ${flags["--name"]} (${flags["--symbol"]}) on Hood`,
60125
+ intent: `Launch ${flags["--name"]} (${flags["--symbol"]}) on Hood${buy ? ` with a first buy of ${buy.display}` : ""}`,
60022
60126
  wallet: wallet.address,
60023
60127
  venue: "curve launch",
60024
60128
  priceImpactPct: null,
60025
- minimumReceived: "0 tokens (no first buy)",
60129
+ minimumReceived: buy ? "not quoted (Candle's buy leg sets its floor on the new curve)" : "0 tokens (no first buy)",
60026
60130
  tokenRisks: [],
60027
- legs: first.plannedLegCount === 2 ? ["create curve", "fee"] : ["create curve"],
60028
- gas: `the createCurve leg up to ${formatUnits(BigInt(leg.gas) * maxFee, 18)} ETH (gas ${leg.gas} at ${formatUnits(maxFee, 9)} gwei)${first.plannedLegCount === 2 ? "; the fee leg is priced by Candle when it becomes next" : ""}`,
60131
+ legs: hoodLaunchLegNames(first.plannedLegCount, buy !== undefined),
60132
+ gas: `the createCurve leg up to ${formatUnits(BigInt(leg.gas) * maxFee, 18)} ETH (gas ${leg.gas} at ${formatUnits(maxFee, 9)} gwei)${hoodLaterLegsGas(first.plannedLegCount, buy !== undefined)}`,
60029
60133
  reserve: `at least ${formatUnits(reserve.wei, 18)} ETH stays in the wallet for a sweep home (${reserve.erc20Transfers} ERC-20 transfers and the final ETH transfer at twice the fee)`,
60030
60134
  curveAddress: curve.address,
60031
60135
  operationId: first.operationId
60032
60136
  };
60033
- ctx.deps.stderr.write(`Launch creates the token only. Make the first buy with a separate candle swap. Price impact does not apply to creation.
60034
- `);
60137
+ ctx.deps.stderr.write(buyNotice(buy, "hood"));
60035
60138
  if (!await confirmQuote(ctx, quote, args.yes)) {
60036
60139
  ctx.deps.stderr.write(`Nothing was signed. Operation ${first.operationId} holds this wallet until ${new Date(first.expiresAt).toISOString()}; a new Hood operation from it is refused as WALLET_BUSY until then.
60037
60140
  `);
@@ -60046,6 +60149,8 @@ async function hoodLaunch(ctx, key, args) {
60046
60149
  walletHeldUntil: first.expiresAt
60047
60150
  });
60048
60151
  }
60152
+ const confirmedPlan = buy ? first.plannedLegCount === 3 ? ["createCurve", "feeTransfer", "trade"] : ["createCurve", "trade"] : undefined;
60153
+ let position = 0;
60049
60154
  const run2 = await runSequencedLegs(ctx, key, {
60050
60155
  wallet,
60051
60156
  first,
@@ -60055,10 +60160,16 @@ async function hoodLaunch(ctx, key, args) {
60055
60160
  clientId: id,
60056
60161
  kind: "launch",
60057
60162
  primaryLeg: "createCurve",
60058
- allowedLegs: HOOD_LAUNCH_LEGS,
60059
- checkLeg: (kind, leg2) => kind !== "createCurve" || leg2.value === "0",
60163
+ allowedLegs,
60164
+ checkLeg: (kind, leg2) => {
60165
+ if (confirmedPlan && confirmedPlan[position++] !== kind)
60166
+ return false;
60167
+ return kind === "createCurve" ? leg2.value === "0" : kind !== "trade" || buy !== undefined && leg2.value === buy.raw && sameEvmAddress(leg2.to, curve.address);
60168
+ },
60060
60169
  onLanded: async () => {}
60061
60170
  });
60171
+ if (confirmedPlan && run2.landed.map((done) => done.kind).join() !== confirmedPlan.join())
60172
+ throw new TradingError("INVALID_RESPONSE", `Candle ended the launch before every confirmed leg landed (${confirmedPlan.join(", ")}).${run2.landed.length === 0 ? " No leg landed." : ` Landed: ${run2.landed.map((done) => `${done.kind} ${safeText(done.hash)}`).join(", ")}.`}`, { details: { operationId: first.operationId, landedLegs: run2.landed } });
60062
60173
  return printTradingResult(ctx, {
60063
60174
  ...run2.final,
60064
60175
  ...await recordLaunchedToken(ctx, wallet, run2.final),
@@ -60071,6 +60182,16 @@ async function hoodLaunch(ctx, key, args) {
60071
60182
  landedLegs: run2.landed
60072
60183
  });
60073
60184
  }
60185
+ function hoodLaterLegsGas(count, buys) {
60186
+ if (count < 2)
60187
+ return "";
60188
+ return buys ? "; the later legs are priced by Candle when each becomes next" : "; the fee leg is priced by Candle when it becomes next";
60189
+ }
60190
+ function hoodLaunchLegNames(count, buys) {
60191
+ if (!buys)
60192
+ return count === 2 ? ["create curve", "fee"] : ["create curve"];
60193
+ return count === 3 ? ["create curve", "fee", "first buy"] : ["create curve", "first buy"];
60194
+ }
60074
60195
  async function recordLaunchedToken(ctx, wallet, final) {
60075
60196
  const mint = typeof final.mint === "string" ? checkEvmAddress(final.mint) : undefined;
60076
60197
  if (!mint?.ok) {
@@ -61633,7 +61754,7 @@ async function perpsDeposit(args, ctx) {
61633
61754
  init_args();
61634
61755
 
61635
61756
  // src/plans.ts
61636
- var PLAN_CAPABILITY_NOTE = "A capability marked yes is what the plan allows. It is subject to the deployment's own switches (perps and own-wallet bridges each have one) and to the wallet, scopes and setup the feature needs.";
61757
+ var PLAN_CAPABILITY_NOTE = "A capability marked yes is what the plan allows. It is subject to the deployment's own switches (perps and own-wallet bridges each have one) and to the wallet, scopes and setup the feature needs. Every plan can launch a token from its embedded wallet, with an optional dev buy (same transaction on Solana; best-effort follow-up on Hood), through the headless launch; the two launch rows are additional routes, not the only ones.";
61637
61758
  var PLAN_CAPABILITY_LABELS = {
61638
61759
  tradeCandleTokens: "Trade Candle-launched tokens",
61639
61760
  tradeBaseAssets: "Trade base assets",
@@ -61641,8 +61762,8 @@ var PLAN_CAPABILITY_LABELS = {
61641
61762
  sellExternalTokens: "Sell tokens not launched on Candle",
61642
61763
  buyExternalTokens: "Buy tokens not launched on Candle",
61643
61764
  hyperliquidPerps: "Hyperliquid perps (when enabled)",
61644
- selfLaunch: "Self-launch from a linked wallet",
61645
- atomicLaunch: "Atomic launch with first buys",
61765
+ selfLaunch: "Launch from a linked or TEE wallet (self-signed)",
61766
+ atomicLaunch: "Atomic launch: launch + 1–4 first buys in one bundle",
61646
61767
  createLinkedWallets: "Create linked wallets",
61647
61768
  importLinkedWallets: "Import linked wallets",
61648
61769
  limitOrders: "Limit orders",
@@ -72327,6 +72448,176 @@ function messageOf3(error) {
72327
72448
  // src/index.ts
72328
72449
  init_wallets();
72329
72450
 
72451
+ // src/commands/wallets-allow-launch.ts
72452
+ init_args();
72453
+ init_deps();
72454
+ init_profiles();
72455
+ init_render();
72456
+ init_promote_support();
72457
+ var PREVIEW_PATH = "/api/v1/agent/tee-wallets/allow-launch/preview";
72458
+ var capabilityPath = (id) => `/api/v1/agent/wallets/${encodeURIComponent(id)}/capabilities`;
72459
+ var USAGE_ALLOW = "Usage: candle wallets allow-launch <label|address|id|prefix*>... [--json]";
72460
+ var USAGE_DISALLOW = "Usage: candle wallets disallow-launch <label|address|id|prefix*>... [--yes] [--json]";
72461
+ var DEVICE_TOKEN_REQUIRED2 = {
72462
+ code: "DEVICE_TOKEN_REQUIRED",
72463
+ message: "Changing whether a TEE wallet may launch needs the device token, the owner's credential; an API key cannot do it.",
72464
+ suggestion: "Run: candle auth login"
72465
+ };
72466
+ var nameOf = (row) => row.label ?? row.address;
72467
+ function allowLaunchTable(rows) {
72468
+ return renderTable(["line", "label", "wallet", "address", "bound key"], rows.map((row, i) => [String(i + 1), row.label ?? "-", row.chain, row.address, row.boundKeyPrefix ?? "-"]));
72469
+ }
72470
+ function nothingToChangeLine(shown, enabled) {
72471
+ const bits = [];
72472
+ if (shown.unchanged.length === 1)
72473
+ bits.push(`that wallet already has allowLaunch ${enabled ? "on" : "off"}`);
72474
+ else if (shown.unchanged.length > 1) {
72475
+ bits.push(`all ${shown.unchanged.length} wallets already have allowLaunch ${enabled ? "on" : "off"}`);
72476
+ }
72477
+ if (shown.notTee.length > 0) {
72478
+ bits.push(`${shown.notTee.map(nameOf).join(", ")} ${shown.notTee.length === 1 ? "is" : "are"} not a TEE wallet`);
72479
+ }
72480
+ if (bits.length === 0)
72481
+ return `Nothing to change.
72482
+ `;
72483
+ return `Nothing to change: ${bits.join("; ")}.
72484
+ `;
72485
+ }
72486
+ function writeFailure2(ctx, result) {
72487
+ const { deps, apiUrl, json } = ctx;
72488
+ let code = result.code;
72489
+ let message = result.message;
72490
+ let suggestion;
72491
+ if (result.status === 404) {
72492
+ code = "ALLOW_LAUNCH_UNSUPPORTED";
72493
+ message = "This Candle API does not offer allowLaunch on TEE wallets (an older API, or TEE launch is off); nothing changed.";
72494
+ } else {
72495
+ const error = result.raw?.error;
72496
+ if (error?.reason === "ambiguous" && Array.isArray(error.matches)) {
72497
+ const ids = error.matches.map((m) => String(m.id)).join(", ");
72498
+ suggestion = `Name one of them by id or address: ${ids}`;
72499
+ } else if (error?.reason === "not_found") {
72500
+ suggestion = "Run: candle wallets, to see this account's linked wallets and their labels.";
72501
+ }
72502
+ }
72503
+ const envelope = errorEnvelope({ ...result, code, message }, { apiUrl, authType: "device" });
72504
+ if (json) {
72505
+ deps.stdout.write(`${JSON.stringify({ ...envelope, ...suggestion ? { suggestion } : {} })}
72506
+ `);
72507
+ } else {
72508
+ deps.stderr.write(`${code ?? `HTTP ${result.status}`}: ${message}${suggestion ? ` ${suggestion}` : ""}
72509
+ `);
72510
+ }
72511
+ return 1;
72512
+ }
72513
+ async function setAllowLaunch(args, ctx, enabled) {
72514
+ const { deps, apiUrl, json } = ctx;
72515
+ const usage6 = enabled ? USAGE_ALLOW : USAGE_DISALLOW;
72516
+ const command = enabled ? "wallets allow-launch" : "wallets disallow-launch";
72517
+ const parsed = parseArgs(args, enabled ? {} : { booleanFlags: ["--yes"] });
72518
+ if ("error" in parsed) {
72519
+ writeUsageFailure(deps, parsed.error, json);
72520
+ return 2;
72521
+ }
72522
+ if (parsed.positionals.length === 0) {
72523
+ writeUsageFailure(deps, `Name at least one wallet. ${usage6}`, json);
72524
+ return 2;
72525
+ }
72526
+ const skipConfirm = !enabled && parsed.booleans.has("--yes");
72527
+ await printIdentity(ctx);
72528
+ const deviceToken = await resolveDeviceToken(deps, ctx.profile);
72529
+ if (!deviceToken) {
72530
+ writeLocalFailure(deps, DEVICE_TOKEN_REQUIRED2, json);
72531
+ return 1;
72532
+ }
72533
+ if (!skipConfirm && (!deps.isTTY.stdin || !deps.isTTY.stdout)) {
72534
+ writeLocalFailure(deps, {
72535
+ code: "ALLOW_LAUNCH_REQUIRES_TTY",
72536
+ message: `candle ${command} needs a terminal: the acknowledgement is typed, and nothing else supplies it.`,
72537
+ suggestion: enabled ? "Run it in an interactive shell; there is no flag and no environment variable for confirm." : "Run it in an interactive shell, or pass --yes to turn it off without the prompt."
72538
+ }, json);
72539
+ return 1;
72540
+ }
72541
+ const request2 = (path, method, body) => apiRequest(path, {
72542
+ method,
72543
+ body,
72544
+ auth: "device",
72545
+ credentials: { deviceToken },
72546
+ apiUrl,
72547
+ fetch: deps.fetch,
72548
+ env: deps.env
72549
+ });
72550
+ const preview = await request2(PREVIEW_PATH, "POST", { wallets: parsed.positionals, enabled });
72551
+ if (!preview.ok)
72552
+ return writeFailure2(ctx, preview);
72553
+ const shown = preview.body;
72554
+ const changing = shown.changed;
72555
+ if (changing.length === 0) {
72556
+ if (json)
72557
+ deps.stdout.write(`${JSON.stringify({ ...shown, command })}
72558
+ `);
72559
+ else
72560
+ deps.stdout.write(nothingToChangeLine(shown, enabled));
72561
+ return shown.unchanged.length === 0 && shown.notTee.length > 0 ? 1 : 0;
72562
+ }
72563
+ const n = changing.length;
72564
+ const screen = [
72565
+ allowLaunchTable(changing),
72566
+ ...shown.unchanged.length > 0 ? [`${shown.unchanged.length} already ${enabled ? "on" : "off"}: ${shown.unchanged.map(nameOf).join(", ")}`] : [],
72567
+ ...shown.notTee.length > 0 ? [`Not a TEE wallet, left alone: ${shown.notTee.map(nameOf).join(", ")}`] : [],
72568
+ "",
72569
+ enabled ? `${n === 1 ? "This wallet" : `These ${n} wallets`} will be allowed to pay for a launch: the bound key, with launch:write, can create tokens from ${n === 1 ? "it" : "them"} (candle launch).` : `${n === 1 ? "This wallet" : `These ${n} wallets`} will no longer be able to pay for a launch. Trading is unchanged.`,
72570
+ ""
72571
+ ];
72572
+ deps.stderr.write(`${screen.join(`
72573
+ `)}
72574
+ `);
72575
+ if (!skipConfirm) {
72576
+ const typed = await deps.promptLine(`Type ${CONFIRM_WORD} to ${enabled ? "allow" : "stop"} launches from ${n === 1 ? "this wallet" : `these ${n} wallets`}: `);
72577
+ if (typed.trim().toLowerCase() !== CONFIRM_WORD) {
72578
+ writeLocalFailure(deps, {
72579
+ code: "ALLOW_LAUNCH_NOT_ACKNOWLEDGED",
72580
+ message: `The acknowledgement is the word ${CONFIRM_WORD}; nothing changed.`,
72581
+ suggestion: `Run the command again and type ${CONFIRM_WORD} at the prompt.`
72582
+ }, json);
72583
+ return 1;
72584
+ }
72585
+ }
72586
+ const done = [];
72587
+ const failed = [];
72588
+ for (const row of changing) {
72589
+ const result = await request2(capabilityPath(row.id), "PUT", { capability: "allowLaunch", enabled });
72590
+ if (result.ok)
72591
+ done.push({ ...row, allowLaunch: enabled });
72592
+ else {
72593
+ failed.push({
72594
+ id: row.id,
72595
+ code: result.status === 404 ? "WALLET_NOT_FOUND" : result.code ?? null,
72596
+ message: result.status === 404 ? "no longer an active TEE wallet on this account" : result.message
72597
+ });
72598
+ }
72599
+ }
72600
+ if (json) {
72601
+ deps.stdout.write(`${JSON.stringify({ success: failed.length === 0, command, enabled, changed: done, unchanged: shown.unchanged, notTee: shown.notTee, failed })}
72602
+ `);
72603
+ return failed.length > 0 ? 1 : 0;
72604
+ }
72605
+ const verb = enabled ? "Allowed launches from" : "Stopped launches from";
72606
+ deps.stdout.write(`${verb} ${done.length} wallet${done.length === 1 ? "" : "s"}.
72607
+ `);
72608
+ for (const failure of failed) {
72609
+ deps.stderr.write(`Not changed: ${failure.id}: ${failure.message}${failure.code ? ` (${failure.code})` : ""}.
72610
+ `);
72611
+ }
72612
+ return failed.length > 0 ? 1 : 0;
72613
+ }
72614
+ function walletsAllowLaunch(args, ctx) {
72615
+ return setAllowLaunch(args, ctx, true);
72616
+ }
72617
+ function walletsDisallowLaunch(args, ctx) {
72618
+ return setAllowLaunch(args, ctx, false);
72619
+ }
72620
+
72330
72621
  // src/commands/wallets-close-empty.ts
72331
72622
  init_args();
72332
72623
  init_render();
@@ -72484,7 +72775,7 @@ init_promote_support();
72484
72775
  var TRUST_PATH = "/api/v1/agent/linked-wallets/trust";
72485
72776
  var USAGE_TRUST = "Usage: candle wallets trust <label|address|id|prefix*>... [--json]";
72486
72777
  var USAGE_UNTRUST = "Usage: candle wallets untrust <label|address|id|prefix*>... [--yes] [--json]";
72487
- var DEVICE_TOKEN_REQUIRED2 = {
72778
+ var DEVICE_TOKEN_REQUIRED3 = {
72488
72779
  code: "DEVICE_TOKEN_REQUIRED",
72489
72780
  message: "Marking a wallet trusted needs the device token, the owner's credential; an API key cannot do it.",
72490
72781
  suggestion: "Run: candle auth login"
@@ -72495,7 +72786,7 @@ function sessionLinkedLine(rows) {
72495
72786
  const names = rows.map((row) => row.label ?? row.address).join(", ");
72496
72787
  return `${n} linked while signed in (${names}): always yours while linked. Revoke ${it} to remove ${it}.`;
72497
72788
  }
72498
- function nothingToChangeLine(shown, trusted) {
72789
+ function nothingToChangeLine2(shown, trusted) {
72499
72790
  const session = shown.sessionLinked ?? [];
72500
72791
  const unchanged = shown.unchanged;
72501
72792
  if (session.length > 0 && unchanged.length === 0) {
@@ -72562,7 +72853,7 @@ async function setTrust(args, ctx, trusted) {
72562
72853
  await printIdentity(ctx);
72563
72854
  const deviceToken = await resolveDeviceToken(deps, ctx.profile);
72564
72855
  if (!deviceToken) {
72565
- writeLocalFailure(deps, DEVICE_TOKEN_REQUIRED2, json);
72856
+ writeLocalFailure(deps, DEVICE_TOKEN_REQUIRED3, json);
72566
72857
  return 1;
72567
72858
  }
72568
72859
  if (!skipConfirm && (!deps.isTTY.stdin || !deps.isTTY.stdout)) {
@@ -72592,7 +72883,7 @@ async function setTrust(args, ctx, trusted) {
72592
72883
  deps.stdout.write(`${JSON.stringify({ ...shown, command })}
72593
72884
  `);
72594
72885
  } else {
72595
- deps.stdout.write(nothingToChangeLine(shown, trusted));
72886
+ deps.stdout.write(nothingToChangeLine2(shown, trusted));
72596
72887
  }
72597
72888
  return 0;
72598
72889
  }
@@ -72859,6 +73150,8 @@ var COMMANDS = {
72859
73150
  revoke: walletsRevoke,
72860
73151
  trust: walletsTrust,
72861
73152
  untrust: walletsUntrust,
73153
+ "allow-launch": walletsAllowLaunch,
73154
+ "disallow-launch": walletsDisallowLaunch,
72862
73155
  "close-empty": walletsCloseEmpty
72863
73156
  },
72864
73157
  bare: wallets
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@candledottv/cli",
3
- "version": "0.11.16",
3
+ "version": "0.11.17",
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": {