candle-creator-bot 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog: candle-creator-bot
2
+
3
+ ## 0.1.0
4
+
5
+ First release, on candle-bot-core 0.4.0 (pinned exactly).
6
+
7
+ - Follows a list of creators (Candle token-list creator or launch signer) through the core's launch source. Each
8
+ creator has an optional label, ticket (at most `buy.sol`), and lifetime caps on entries and SOL, counted from the
9
+ ledger's positions for that creator with the core's rules for what spent. The core's caps on top.
10
+ - Global filters as candle-launch-bot's: launch modes, max age, impact on the curve; a chain-first launch waits for
11
+ Candle's market record; creator unknown waits. Optional: wait for the creator's own dev buy.
12
+ - `run`, `status` (per-creator use), `sell`; `STOP`, `PAUSE_BUYS`; a `follows` table in the ledger.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 candle-launch-bot contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,115 @@
1
- # Temporary Holding Version
1
+ # candle-creator-bot
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Buys the new [Candle](https://candle.tv) launches of the creators you list, with a ticket and caps per creator, and
4
+ exits them by a strategy you pick. One config file, one process, a local SQLite ledger. Built on Candle's official SDK,
5
+ [`@candledottv/agent-sdk`](https://www.npmjs.com/package/@candledottv/agent-sdk) (pinned at 0.6.14), and
6
+ [candle-bot-core](https://www.npmjs.com/package/candle-bot-core), the engine
7
+ [candle-launch-bot](https://www.npmjs.com/package/candle-launch-bot) runs on.
8
+
9
+ Built by [Bruce Jeet](https://github.com/BruceJeet).
10
+
11
+ **How it differs from candle-launch-bot with `creatorAllow`:** the list of creators is the main input, not a filter
12
+ on top of every launch: each creator has its own label, ticket and lifetime caps (entries and SOL), and `status.json`
13
+ shows what each creator has used.
14
+
15
+ - **What it buys:** a new Candle launch whose creator (from Candle's token list) or signer (the launch transaction's
16
+ fee payer) is on your list. A launch whose creator is not known yet waits, until Candle's list names it or the launch
17
+ ages out. Never a launch by your own wallet.
18
+ - **Per creator:** `sol` (the ticket, at most `buy.sol`), `maxEntries` and `maxSol` over the bot's lifetime, on top of
19
+ the core's caps (`buy.maxOpen`, `buy.lifetimeSol`, `buy.maxPerCoin`). A creator's caps can only tighten them.
20
+ - **Global filters**, as candle-launch-bot's: launch modes, max age, max impact of the ticket on the curve. Optional:
21
+ wait for the creator's own dev buy.
22
+ - **Detection, execution, exits and dry mode** as candle-launch-bot: Candle's token list and the curve program on
23
+ chain; Candle's trade API from the embedded wallet or a TEE wallet; fills from the transaction; `hold`, `tpsl`,
24
+ `time`, `ladder`, `preGrad` or your own file.
25
+
26
+ Solana only.
27
+
28
+ ## Quick start
29
+
30
+ Node 22.18 or newer.
31
+
32
+ ```sh
33
+ npm install -g candle-creator-bot # or npx candle-creator-bot ...
34
+ cp "$(npm root -g)/candle-creator-bot/examples/dry.json" bot.json # put your creators in follow.creators
35
+ candle-creator-bot run --config bot.json # dry run: the whole loop with paper fills
36
+ ```
37
+
38
+ For live: export the API key (`apiKeyEnv`), fund the wallet, set `"mode": "live"` and your `wallet`, run again.
39
+
40
+ ## What it needs
41
+
42
+ - A **Candle API key** with the `swap:write` scope, in an environment variable (`apiKeyEnv`).
43
+ - A **payer**: `embedded` (the account's embedded wallet; Candle signs) or `linked` (a TEE wallet signed on this
44
+ machine). Setup is the same as candle-launch-bot's; its README walks through it.
45
+ - An **RPC** (`rpcUrlEnv`); the public one works for a dry run.
46
+ - The **creators**: Candle wallet addresses. A launch matches when Candle's token list names the address as creator,
47
+ or when the address signed (paid for) the launch transaction.
48
+
49
+ ## Config reference
50
+
51
+ The core's sections (`mode`, `payer`, `apiKeyEnv`, `rpcUrlEnv`, `wallet`, `signer`, `stateDir`, `buy`, `sell`, `exit`,
52
+ `retry`, `alerts`) are the same as candle-launch-bot's. This bot adds:
53
+
54
+ ```json
55
+ {
56
+ "follow": {
57
+ "creators": [
58
+ { "address": "<creator address>", "label": "dev one", "sol": 0.01, "maxEntries": 5, "maxSol": 0.05 },
59
+ { "address": "<another>" }
60
+ ],
61
+ "launchModes": ["open", "exclusive", "exp-a", "exp-b", "exp-c", "exp-custom"],
62
+ "maxAgeSec": 60,
63
+ "maxImpactBps": 300,
64
+ "inferModeFromCurve": true,
65
+ "requireDevBuy": false
66
+ },
67
+ "detect": { "api": true, "chain": true, "apiPollMs": 3000, "chainPollMs": 15000, "apiLimit": 20 }
68
+ }
69
+ ```
70
+
71
+ | Key | Meaning |
72
+ |---|---|
73
+ | `follow.creators[].address` | the creator (token list) or launch signer to follow; each listed once |
74
+ | `follow.creators[].label` | a name for logs, the `follows` table and `status.json` (optional) |
75
+ | `follow.creators[].sol` | this creator's ticket (default `buy.sol`, never above it) |
76
+ | `follow.creators[].maxEntries` / `maxSol` | lifetime caps for this creator (optional). Counted from the ledger's positions for the creator, as the core counts `lifetimeSol`: a buy Candle refused before anything was signed, or one that reverted, does not count |
77
+ | `follow.launchModes` | modes to buy (default every production mode) |
78
+ | `follow.maxAgeSec` | buy only this soon after the launch (default 60); no order is built after it |
79
+ | `follow.maxImpactBps` | impact of the creator's ticket on the curve (default 300); also the buy-retry limit |
80
+ | `follow.inferModeFromCurve` | decide a chain-first launch's mode from its curve (only the full `open` preset) instead of waiting for Candle's list |
81
+ | `follow.requireDevBuy` | buy only once the listed creator's own wallet holds the coin |
82
+ | `detect.*` | the launch source, as candle-launch-bot's |
83
+
84
+ ## Controls
85
+
86
+ `STOP` and `PAUSE_BUYS` files in the state dir; `candle-creator-bot sell <mint> [--fraction f]`. A restart picks up from
87
+ the ledger, the creators' caps included.
88
+
89
+ ## State dir
90
+
91
+ | File | What |
92
+ |---|---|
93
+ | `ledger.sqlite` | positions (each with the creator it followed in its entry context), orders, attempts, `follows` (every launch of a listed creator with the decision), alerts |
94
+ | `status.json` | equity, open positions, caps used, `follow.creators` (entries and SOL used per creator), pending launches, recent decisions |
95
+ | `events.jsonl` | one JSON line per event: `follow_buy`, `follow_skip`, `follow_wait`, `buy_order`, fills, exits |
96
+
97
+ ## Not here yet
98
+
99
+ - **Selling when the creator sells.** Exits are the strategy's. A creator-sell exit needs a wallet watcher (the
100
+ creator's token account changes, from the chain); it would plug in as an exit source that marks the position for a
101
+ sell, the way a strategy's `sell` decision does.
102
+
103
+ ## Known limits
104
+
105
+ - **Creator unknown waits.** A launch seen on chain first carries only its signer; if the signer is not on the list,
106
+ the bot waits for Candle's token list to name the creator, up to `follow.maxAgeSec`.
107
+ - **Dev buy check.** `requireDevBuy` reads the listed creator's token balance in the coin, at most every 2 s. It
108
+ measures a holding, not a buy: it sees a dev buy in the launch transaction and one in the same block after it, and it
109
+ cannot tell a dev buy from a later buy, or from tokens handed to the creator's wallet at launch.
110
+ - **One entry per coin.** `buy.maxPerCoin` is 1.
111
+ - **Solana only.**
112
+
113
+ ## License
114
+
115
+ MIT
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ // The published package runs the compiled dist/; a source checkout runs src/ directly (Node strips types).
3
+ import { existsSync } from "node:fs";
4
+ const dist = new URL("../dist/cli.js", import.meta.url);
5
+ const entry = existsSync(dist) ? dist : new URL("../src/cli.ts", import.meta.url);
6
+ const { main } = await import(entry.href);
7
+ process.exit(await main(process.argv.slice(2)));
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export declare const HELP = "candle-creator-bot: buys the new Candle launches of the creators you list, with a ticket and caps per creator,\nand exits them by a strategy.\n\n candle-creator-bot run --config bot.json [--mode dry|live] run the bot\n candle-creator-bot status --config bot.json print status.json (each creator's entries and SOL used)\n candle-creator-bot sell <mint> --config bot.json [--fraction 1]\n\nStart from examples/dry.json and put your creators in follow.creators. Controls: STOP or PAUSE_BUYS in the state folder.\nDocs: the README.\n";
2
+ /** The entry point: never lets an error out unredacted (the SDK puts RPC URLs in its messages). */
3
+ export declare function main(argv?: string[]): Promise<number>;
package/dist/cli.js ADDED
@@ -0,0 +1,70 @@
1
+ /** The command line: run, status, sell. */
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { args, context as coreContext, die, guardMain, parts, run, sell, status } from "candle-bot-core/runtime";
5
+ import { Detector } from "candle-bot-core/sources/candleLaunches";
6
+ import { loadConfig, ConfigError } from "./config.js";
7
+ import { Ledger } from "./ledger.js";
8
+ import { Engine } from "./trigger.js";
9
+ export const HELP = `candle-creator-bot: buys the new Candle launches of the creators you list, with a ticket and caps per creator,
10
+ and exits them by a strategy.
11
+
12
+ candle-creator-bot run --config bot.json [--mode dry|live] run the bot
13
+ candle-creator-bot status --config bot.json print status.json (each creator's entries and SOL used)
14
+ candle-creator-bot sell <mint> --config bot.json [--fraction 1]
15
+
16
+ Start from examples/dry.json and put your creators in follow.creators. Controls: STOP or PAUSE_BUYS in the state folder.
17
+ Docs: the README.
18
+ `;
19
+ function context(configPath, modeOverride) {
20
+ let cfg;
21
+ try {
22
+ cfg = loadConfig(configPath);
23
+ }
24
+ catch (e) {
25
+ if (e instanceof ConfigError)
26
+ die(e.message);
27
+ throw e;
28
+ }
29
+ return coreContext(cfg, modeOverride);
30
+ }
31
+ async function makeEngine(ctx, ledger) {
32
+ const { strategy, alerter, executor } = await parts(ctx, ledger, { maxBuyImpactBps: ctx.cfg.follow.maxImpactBps });
33
+ const d = ctx.cfg.detect;
34
+ const source = new Detector({ rpc: ctx.rpc, wsUrl: d.chain ? ctx.wsUrl : undefined, apiUrl: ctx.cfg.apiUrl, log: ctx.log }, d);
35
+ const engine = new Engine({ cfg: ctx.cfg, ledger, source, executor, strategy, rpc: ctx.rpc, log: ctx.log, alerter, markets: ctx.client });
36
+ return { engine, trigger: engine.trigger };
37
+ }
38
+ const openLedger = (ctx) => () => new Ledger(join(ctx.cfg.stateDir, "ledger.sqlite"));
39
+ /** The entry point: never lets an error out unredacted (the SDK puts RPC URLs in its messages). */
40
+ export async function main(argv = process.argv.slice(2)) {
41
+ return guardMain(async () => {
42
+ const { pos, opt } = args(argv);
43
+ const cmd = pos[0];
44
+ if (!cmd || cmd === "help" || opt.help) {
45
+ process.stdout.write(HELP);
46
+ return cmd || opt.help ? 0 : 2;
47
+ }
48
+ if (cmd === "version" || opt.version) {
49
+ process.stdout.write(JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version + "\n");
50
+ return 0;
51
+ }
52
+ const config = opt.config ?? "bot.json";
53
+ switch (cmd) {
54
+ case "run": {
55
+ const ctx = context(config, opt.mode);
56
+ return run(ctx, openLedger(ctx), (ledger) => makeEngine(ctx, ledger), { creators: ctx.cfg.follow.creators.length });
57
+ }
58
+ case "status":
59
+ return status(context(config));
60
+ case "sell": {
61
+ if (!pos[1])
62
+ die("sell <mint> [--fraction f]");
63
+ const ctx = context(config);
64
+ return sell(ctx, pos[1], opt.fraction ? Number(opt.fraction) : 1, openLedger(ctx), (ledger) => makeEngine(ctx, ledger));
65
+ }
66
+ default:
67
+ die(`unknown command ${cmd}`);
68
+ }
69
+ });
70
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The one config file: the core's shared sections (candle-bot-core) plus `follow` (the creators and the global filters)
3
+ * and `detect` (where launches are seen). Unknown keys are refused at every level.
4
+ */
5
+ import { type Config as CoreConfig, type SectionParser } from "candle-bot-core";
6
+ export { ConfigError } from "candle-bot-core/config";
7
+ export interface Creator {
8
+ address: string;
9
+ label?: string;
10
+ /** This creator's ticket, SOL; at most buy.sol (default buy.sol). */
11
+ sol: number;
12
+ /** Most buys of this creator's launches over the bot's lifetime (optional). */
13
+ maxEntries?: number;
14
+ /** Most SOL spent on this creator's launches over the bot's lifetime (optional). */
15
+ maxSol?: number;
16
+ }
17
+ export interface Follow {
18
+ creators: Creator[];
19
+ launchModes: string[];
20
+ maxAgeSec: number;
21
+ maxImpactBps: number;
22
+ inferModeFromCurve: boolean;
23
+ /** Buy only once the creator's wallet holds the coin (its own dev buy). */
24
+ requireDevBuy: boolean;
25
+ }
26
+ export interface Detect {
27
+ api: boolean;
28
+ chain: boolean;
29
+ apiPollMs: number;
30
+ chainPollMs: number;
31
+ apiLimit: number;
32
+ }
33
+ export type Config = CoreConfig & {
34
+ follow: Follow;
35
+ detect: Detect;
36
+ };
37
+ /** Candle's launch modes (agent-sdk 0.6.14 launch-definition `curve.values`). */
38
+ export declare const LAUNCH_MODES: string[];
39
+ /** Every production mode: the default, since the list of creators is the main filter. */
40
+ export declare const DEFAULT_MODES: string[];
41
+ declare function parseDetect(raw: unknown): Detect;
42
+ /** This bot's sections, in the order they are parsed (after `sell`, before `exit`). */
43
+ export declare const SECTIONS: {
44
+ follow: SectionParser<Follow>;
45
+ detect: typeof parseDetect;
46
+ };
47
+ export declare function parseConfig(raw: unknown, baseDir?: string): Config;
48
+ export declare function loadConfig(path: string): Config;
package/dist/config.js ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The one config file: the core's shared sections (candle-bot-core) plus `follow` (the creators and the global filters)
3
+ * and `detect` (where launches are seen). Unknown keys are refused at every level.
4
+ */
5
+ import { parseConfig as parseCore, loadConfig as loadCore, ConfigError, isObj, only, num, bool, str, strList, asAddress } from "candle-bot-core";
6
+ export { ConfigError } from "candle-bot-core/config";
7
+ /** Candle's launch modes (agent-sdk 0.6.14 launch-definition `curve.values`). */
8
+ export const LAUNCH_MODES = ["open", "exclusive", "test-open", "test-exclusive", "exp-a", "exp-b", "exp-c", "exp-custom"];
9
+ /** Every production mode: the default, since the list of creators is the main filter. */
10
+ export const DEFAULT_MODES = ["open", "exclusive", "exp-a", "exp-b", "exp-c", "exp-custom"];
11
+ const parseFollow = (raw, core) => {
12
+ if (!isObj(raw))
13
+ throw new ConfigError("config.follow: required object with a `creators` list");
14
+ only(raw, ["creators", "launchModes", "maxAgeSec", "maxImpactBps", "inferModeFromCurve", "requireDevBuy"], "config.follow");
15
+ const list = raw.creators;
16
+ if (!Array.isArray(list) || !list.length)
17
+ throw new ConfigError("config.follow.creators: required, a list of { address, label?, sol?, maxEntries?, maxSol? }");
18
+ const seen = new Set();
19
+ const creators = list.map((c, i) => {
20
+ const where = `config.follow.creators[${i}]`;
21
+ if (!isObj(c))
22
+ throw new ConfigError(`${where}: must be an object`);
23
+ only(c, ["address", "label", "sol", "maxEntries", "maxSol"], where);
24
+ const address = str(c, "address", where);
25
+ if (!asAddress(address))
26
+ throw new ConfigError(`${where}.address: "${address}" is not an address`);
27
+ if (seen.has(address))
28
+ throw new ConfigError(`${where}.address: listed twice`);
29
+ seen.add(address);
30
+ const sol = num(c, "sol", where, { min: 0.000001, def: core.buy.sol });
31
+ if (sol > core.buy.sol)
32
+ throw new ConfigError(`${where}.sol: ${sol} is above buy.sol (${core.buy.sol}); a creator's ticket can only be smaller`);
33
+ return {
34
+ address,
35
+ label: str(c, "label", where, { optional: true }),
36
+ sol,
37
+ maxEntries: c.maxEntries === undefined ? undefined : num(c, "maxEntries", where, { min: 1, int: true }),
38
+ maxSol: c.maxSol === undefined ? undefined : num(c, "maxSol", where, { min: 0.000001 }),
39
+ };
40
+ });
41
+ for (const [i, c] of creators.entries()) {
42
+ if (c.maxSol !== undefined && c.maxSol < c.sol)
43
+ throw new ConfigError(`config.follow.creators[${i}].maxSol: ${c.maxSol} is below its ticket ${c.sol}; it could never buy`);
44
+ }
45
+ return finish(creators, raw);
46
+ };
47
+ function finish(creators, raw) {
48
+ const launchModes = strList(raw, "launchModes", "config.follow", { def: DEFAULT_MODES });
49
+ if (!launchModes.length)
50
+ throw new ConfigError("config.follow.launchModes: at least one mode");
51
+ for (const m of launchModes)
52
+ if (!LAUNCH_MODES.includes(m))
53
+ throw new ConfigError(`config.follow.launchModes: "${m}" is not a Candle launch mode (${LAUNCH_MODES.join(", ")})`);
54
+ return {
55
+ creators,
56
+ launchModes,
57
+ maxAgeSec: num(raw, "maxAgeSec", "config.follow", { min: 1, def: 60 }),
58
+ maxImpactBps: num(raw, "maxImpactBps", "config.follow", { min: 0, max: 10_000, def: 300 }),
59
+ inferModeFromCurve: bool(raw, "inferModeFromCurve", "config.follow", true),
60
+ requireDevBuy: bool(raw, "requireDevBuy", "config.follow", false),
61
+ };
62
+ }
63
+ ;
64
+ function parseDetect(raw) {
65
+ const d = raw === undefined ? {} : raw;
66
+ if (!isObj(d))
67
+ throw new ConfigError("config.detect: must be an object");
68
+ only(d, ["api", "chain", "apiPollMs", "chainPollMs", "apiLimit"], "config.detect");
69
+ const detect = {
70
+ api: bool(d, "api", "config.detect", true),
71
+ chain: bool(d, "chain", "config.detect", true),
72
+ apiPollMs: num(d, "apiPollMs", "config.detect", { min: 500, int: true, def: 3000 }),
73
+ chainPollMs: num(d, "chainPollMs", "config.detect", { min: 1000, int: true, def: 15_000 }),
74
+ apiLimit: num(d, "apiLimit", "config.detect", { min: 1, max: 200, int: true, def: 20 }),
75
+ };
76
+ if (!detect.api && !detect.chain)
77
+ throw new ConfigError("config.detect: at least one of api and chain must be on");
78
+ return detect;
79
+ }
80
+ /** This bot's sections, in the order they are parsed (after `sell`, before `exit`). */
81
+ export const SECTIONS = { follow: parseFollow, detect: parseDetect };
82
+ export function parseConfig(raw, baseDir = process.cwd()) {
83
+ return parseCore(raw, baseDir, SECTIONS);
84
+ }
85
+ export function loadConfig(path) {
86
+ return loadCore(path, SECTIONS);
87
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The global filters (launch mode, age, impact of the creator's ticket on the curve), as candle-launch-bot's: a
3
+ * verdict is `buy`, `skip` (final) or `wait` (a field is not known yet). Unknown never passes. A copy of the launch
4
+ * bot's checks for these three filters (this bot installs alone, without the launch bot).
5
+ */
6
+ import type { CandleCurve } from "candle-bot-core/market/curve";
7
+ import type { Launch } from "candle-bot-core/sources/candleLaunches";
8
+ import type { Follow } from "./config.ts";
9
+ export type FilterVerdict = {
10
+ verdict: "buy";
11
+ impactBps: number;
12
+ expectedOutRaw: bigint;
13
+ } | {
14
+ verdict: "skip";
15
+ reason: string;
16
+ } | {
17
+ verdict: "wait";
18
+ reason: string;
19
+ };
20
+ /** Age and launch mode: what needs no curve read (a chain-first launch's mode may need it: then it waits for one). */
21
+ export declare function ageAndMode(f: Follow, l: Launch, curve: CandleCurve | null | "unreadable", nowMs: number): Exclude<FilterVerdict, {
22
+ verdict: "buy";
23
+ }> | null;
24
+ /** The curve checks and the impact of `lamports` on it. */
25
+ export declare function onCurve(f: Follow, l: Launch, curve: CandleCurve | null | "unreadable", lamports: bigint): FilterVerdict;
@@ -0,0 +1,53 @@
1
+ import { buyImpact, curveClass } from "candle-bot-core/market/curve";
2
+ import { WSOL } from "candle-bot-core/solana/pda";
3
+ /** Age and launch mode: what needs no curve read (a chain-first launch's mode may need it: then it waits for one). */
4
+ export function ageAndMode(f, l, curve, nowMs) {
5
+ if (l.chain !== "solana")
6
+ return { verdict: "skip", reason: `chain ${l.chain}` };
7
+ if (l.launchedAtMs === null)
8
+ return { verdict: "skip", reason: "launch time unknown" };
9
+ const ageSec = (nowMs - l.launchedAtMs) / 1000;
10
+ if (ageSec > f.maxAgeSec)
11
+ return { verdict: "skip", reason: `age ${Math.round(ageSec)}s > ${f.maxAgeSec}s` };
12
+ if (!l.launchMode) {
13
+ if (!f.inferModeFromCurve)
14
+ return { verdict: "wait", reason: "launch mode unknown" };
15
+ if (curve === null)
16
+ return { verdict: "wait", reason: "curve not readable yet" };
17
+ if (curve === "unreadable")
18
+ return { verdict: "skip", reason: "curve unreadable" };
19
+ // Only a full match of the open preset decides here; every other curve waits for Candle's `tier`.
20
+ if (curveClass(curve) !== "open")
21
+ return { verdict: "wait", reason: "launch mode unknown" };
22
+ if (!f.launchModes.includes("open"))
23
+ return { verdict: "skip", reason: "mode open (from the curve preset)" };
24
+ }
25
+ else if (!f.launchModes.includes(l.launchMode))
26
+ return { verdict: "skip", reason: `mode ${l.launchMode}` };
27
+ return null;
28
+ }
29
+ /** The curve checks and the impact of `lamports` on it. */
30
+ export function onCurve(f, l, curve, lamports) {
31
+ if (curve === null)
32
+ return { verdict: "wait", reason: "curve not readable yet" };
33
+ if (curve === "unreadable")
34
+ return { verdict: "skip", reason: "curve unreadable" };
35
+ if (curve.baseMint !== l.mint)
36
+ return { verdict: "skip", reason: "curve names another mint" };
37
+ if (curve.quoteMint !== WSOL)
38
+ return { verdict: "skip", reason: "curve is not SOL-quoted" };
39
+ if (curve.migrated)
40
+ return { verdict: "skip", reason: "already graduated" };
41
+ if (curve.quoteReserve >= curve.migrationThreshold)
42
+ return { verdict: "skip", reason: "curve full (migrating)" };
43
+ let r;
44
+ try {
45
+ r = buyImpact(curve, lamports);
46
+ }
47
+ catch (e) {
48
+ return { verdict: "skip", reason: e.message };
49
+ }
50
+ if (r.impactBps > f.maxImpactBps)
51
+ return { verdict: "skip", reason: `impact ${r.impactBps} bps > ${f.maxImpactBps}` };
52
+ return { verdict: "buy", impactBps: r.impactBps, expectedOutRaw: r.expectedOutRaw };
53
+ }
@@ -0,0 +1,10 @@
1
+ /** candle-creator-bot: the trigger, the filters, the config and the ledger, for use as a library. */
2
+ export { CreatorTrigger, Engine, candleHasMarket, followedOf, NO_CANDLE_MARKET } from "./trigger.ts";
3
+ export type { TriggerDeps, EngineDeps, LaunchSource, MarketInfoSource } from "./trigger.ts";
4
+ export { ageAndMode, onCurve } from "./filters.ts";
5
+ export type { FilterVerdict } from "./filters.ts";
6
+ export { parseConfig, loadConfig, SECTIONS, ConfigError, LAUNCH_MODES, DEFAULT_MODES } from "./config.ts";
7
+ export type { Config, Creator, Follow, Detect } from "./config.ts";
8
+ export { Ledger } from "./ledger.ts";
9
+ export type { FollowRow } from "./ledger.ts";
10
+ export { main, HELP } from "./cli.ts";
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ /** candle-creator-bot: the trigger, the filters, the config and the ledger, for use as a library. */
2
+ export { CreatorTrigger, Engine, candleHasMarket, followedOf, NO_CANDLE_MARKET } from "./trigger.js";
3
+ export { ageAndMode, onCurve } from "./filters.js";
4
+ export { parseConfig, loadConfig, SECTIONS, ConfigError, LAUNCH_MODES, DEFAULT_MODES } from "./config.js";
5
+ export { Ledger } from "./ledger.js";
6
+ export { main, HELP } from "./cli.js";
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The creator follower's ledger: the core ledger (candle-bot-core) plus `follows`, one row per launch of a listed
3
+ * creator with the decision. A creator's usage (entries and SOL) is read from the core's own positions and orders,
4
+ * by the `followed` address the bot writes into each position's entry context: the same counting rules as the core's
5
+ * maxPerCoin and lifetimeSol (a buy Candle refused before anything was signed, or one that reverted, spent nothing and
6
+ * does not count), and nothing a booking could miss or a restart could lose.
7
+ */
8
+ import { Ledger as CoreLedger } from "candle-bot-core/ledger";
9
+ export type { PositionRow, OrderRow, PositionStatus, OrderStatus, Side, QtySource } from "candle-bot-core/ledger";
10
+ export interface FollowRow {
11
+ mint: string;
12
+ creator: string;
13
+ label: string | null;
14
+ launched_at: number | null;
15
+ first_seen_at: number;
16
+ symbol: string | null;
17
+ launch_mode: string | null;
18
+ decision: string;
19
+ reason: string;
20
+ }
21
+ export declare class Ledger extends CoreLedger {
22
+ constructor(path: string);
23
+ recordFollow(r: FollowRow): void;
24
+ follow(mint: string): FollowRow | undefined;
25
+ recentFollows(limit?: number): FollowRow[];
26
+ /**
27
+ * What this mode has committed to a followed creator: entries (positions that may have spent) and lamports (their
28
+ * buy tickets, one per transaction on a double execution), from the positions whose entry context names it.
29
+ */
30
+ creatorUsage(creator: string, dry: boolean): {
31
+ entries: number;
32
+ lamports: bigint;
33
+ };
34
+ }
package/dist/ledger.js ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The creator follower's ledger: the core ledger (candle-bot-core) plus `follows`, one row per launch of a listed
3
+ * creator with the decision. A creator's usage (entries and SOL) is read from the core's own positions and orders,
4
+ * by the `followed` address the bot writes into each position's entry context: the same counting rules as the core's
5
+ * maxPerCoin and lifetimeSol (a buy Candle refused before anything was signed, or one that reverted, spent nothing and
6
+ * does not count), and nothing a booking could miss or a restart could lose.
7
+ */
8
+ import { Ledger as CoreLedger } from "candle-bot-core/ledger";
9
+ const FOLLOWS = `
10
+ create table if not exists follows (
11
+ mint text primary key, creator text not null, label text, launched_at integer, first_seen_at integer not null,
12
+ symbol text, launch_mode text, decision text not null, reason text not null
13
+ );
14
+ `;
15
+ export class Ledger extends CoreLedger {
16
+ constructor(path) {
17
+ super(path);
18
+ this.db.exec(FOLLOWS);
19
+ }
20
+ recordFollow(r) {
21
+ this.db
22
+ .prepare(`insert into follows (mint, creator, label, launched_at, first_seen_at, symbol, launch_mode, decision, reason)
23
+ values (?, ?, ?, ?, ?, ?, ?, ?, ?)
24
+ on conflict(mint) do update set decision = excluded.decision, reason = excluded.reason,
25
+ symbol = coalesce(excluded.symbol, follows.symbol), launch_mode = coalesce(excluded.launch_mode, follows.launch_mode)`)
26
+ .run(r.mint, r.creator, r.label, r.launched_at, r.first_seen_at, r.symbol, r.launch_mode, r.decision, r.reason);
27
+ }
28
+ follow(mint) {
29
+ return this.db.prepare("select * from follows where mint = ?").get(mint);
30
+ }
31
+ recentFollows(limit = 20) {
32
+ return this.db.prepare("select * from follows order by first_seen_at desc limit ?").all(limit);
33
+ }
34
+ /**
35
+ * What this mode has committed to a followed creator: entries (positions that may have spent) and lamports (their
36
+ * buy tickets, one per transaction on a double execution), from the positions whose entry context names it.
37
+ */
38
+ creatorUsage(creator, dry) {
39
+ const rows = this.db
40
+ .prepare(`select p.id pid, o.amount_raw a, o.extra_signatures x from positions p join orders o on o.position_id = p.id
41
+ where o.side = 'buy' and p.dry = ? and json_extract(p.launch_json, '$.followed') = ?
42
+ and o.status != 'reverted' and not (o.status = 'rejected' and o.ever_signed = 0)`)
43
+ .all(dry ? 1 : 0, creator);
44
+ const lamports = rows.reduce((s, r) => s + BigInt(r.a) * BigInt(1 + (r.x ? JSON.parse(r.x).length : 0)), 0n);
45
+ return { entries: new Set(rows.map((r) => r.pid)).size, lamports };
46
+ }
47
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The creator follower's trigger on the core engine (candle-bot-core): new Candle launches come from the core's launch
3
+ * source; a launch whose creator (Candle's token list) or signer (the launch transaction's fee payer) is on the list is
4
+ * checked against the global filters, the creator's own caps and, optionally, the creator's dev buy, and bought with
5
+ * the creator's ticket. A launch whose creator is not known yet waits, as in candle-launch-bot. Every decision on a
6
+ * listed creator's launch is a row in `follows`.
7
+ */
8
+ import { Engine as CoreEngine } from "candle-bot-core/engine";
9
+ import type { Candidate, PendingInfo, Trigger, TriggerContext, Verdict } from "candle-bot-core/trigger";
10
+ import type { Alerter } from "candle-bot-core/alerts";
11
+ import type { Executor } from "candle-bot-core/exec/executor";
12
+ import type { Logger } from "candle-bot-core/log";
13
+ import { type Rpc } from "candle-bot-core/solana/rpc";
14
+ import type { Launch } from "candle-bot-core/sources/candleLaunches";
15
+ import type { ExitStrategy } from "candle-bot-core/strategies/types";
16
+ import type { Config, Creator } from "./config.ts";
17
+ import type { Ledger } from "./ledger.ts";
18
+ /** What the trigger needs from a launch source (the core's CandleLaunches; a fake in tests). */
19
+ export interface LaunchSource {
20
+ start(): Promise<void>;
21
+ stop(): void;
22
+ drain(): Launch[];
23
+ get(mint: string): Launch | undefined;
24
+ onNew: (() => void) | null;
25
+ readonly stats?: unknown;
26
+ }
27
+ export interface MarketInfoSource {
28
+ /** Candle's public market record (`tier`, `candleLaunched`, `trade.routable`). */
29
+ getMarket(chain: "solana", mint: string): Promise<any>;
30
+ }
31
+ export declare const NO_CANDLE_MARKET = "no Candle market";
32
+ export declare const MARKET_CHECK_MS = 2000;
33
+ export declare const MARKET_READ_TIMEOUT_MS = 3000;
34
+ export declare const DEV_BUY_CHECK_MS = 2000;
35
+ /** As candle-launch-bot's: only an explicit `candleLaunched: true` passes; `trade.routable: false` refuses. */
36
+ export declare function candleHasMarket(m: any): {
37
+ ok: true;
38
+ } | {
39
+ ok: false;
40
+ why: string;
41
+ };
42
+ export interface TriggerDeps {
43
+ cfg: Config;
44
+ ledger: Ledger;
45
+ source: LaunchSource;
46
+ rpc: Rpc;
47
+ log: Logger;
48
+ markets?: MarketInfoSource | null;
49
+ now?: () => number;
50
+ }
51
+ /** The listed creator of a launch: the token list's creator first, then the launch transaction's signer. */
52
+ export declare function followedOf(creators: Creator[], l: Launch): Creator | undefined;
53
+ export declare class CreatorTrigger implements Trigger {
54
+ readonly name = "creator";
55
+ readonly candidateLabel = "launches";
56
+ readonly buyIdPrefix = "ccb";
57
+ readonly sellIdPrefix = "ccs";
58
+ /** One decision at a time: a creator's caps are read from the ledger, and the booking of one buy lands before the next is decided. */
59
+ readonly concurrency = 1;
60
+ private d;
61
+ private now;
62
+ private state;
63
+ constructor(d: TriggerDeps);
64
+ start(ctx?: TriggerContext): Promise<void>;
65
+ stop(): void;
66
+ poll(): Candidate[];
67
+ private launchOf;
68
+ private st;
69
+ decide(c: Candidate, ctx: TriggerContext, pending: PendingInfo): Promise<Verdict>;
70
+ /** The listed creator's own wallet holds the coin (not the launch's signer, which is often a payer wallet). */
71
+ private devBought;
72
+ private readMarket;
73
+ private noteMarket;
74
+ onVerdict(c: Candidate, v: Verdict, _ctx: TriggerContext, pending: PendingInfo): void;
75
+ status(pending: Array<{
76
+ candidate: Candidate;
77
+ } & PendingInfo>): Record<string, unknown>;
78
+ }
79
+ export interface EngineDeps extends TriggerDeps {
80
+ executor: Executor;
81
+ strategy: ExitStrategy;
82
+ alerter: Alerter;
83
+ }
84
+ /** The creator follower's engine: the core engine with the creator trigger. */
85
+ export declare class Engine extends CoreEngine {
86
+ constructor(d: EngineDeps);
87
+ }
@@ -0,0 +1,243 @@
1
+ /**
2
+ * The creator follower's trigger on the core engine (candle-bot-core): new Candle launches come from the core's launch
3
+ * source; a launch whose creator (Candle's token list) or signer (the launch transaction's fee payer) is on the list is
4
+ * checked against the global filters, the creator's own caps and, optionally, the creator's dev buy, and bought with
5
+ * the creator's ticket. A launch whose creator is not known yet waits, as in candle-launch-bot. Every decision on a
6
+ * listed creator's launch is a row in `follows`.
7
+ */
8
+ import { Engine as CoreEngine, lamportsOf } from "candle-bot-core/engine";
9
+ import { candleCurve } from "candle-bot-core/solana/pda";
10
+ import { tokenBalance } from "candle-bot-core/solana/rpc";
11
+ import { ageAndMode, onCurve } from "./filters.js";
12
+ export const NO_CANDLE_MARKET = "no Candle market";
13
+ export const MARKET_CHECK_MS = 2_000;
14
+ export const MARKET_READ_TIMEOUT_MS = 3_000;
15
+ export const DEV_BUY_CHECK_MS = 2_000;
16
+ /** As candle-launch-bot's: only an explicit `candleLaunched: true` passes; `trade.routable: false` refuses. */
17
+ export function candleHasMarket(m) {
18
+ if (!m || typeof m !== "object")
19
+ return { ok: false, why: "no market record" };
20
+ if (m.candleLaunched !== true)
21
+ return { ok: false, why: m.candleLaunched === false ? "not a Candle market" : "candleLaunched unknown" };
22
+ if (m.trade && m.trade.routable === false)
23
+ return { ok: false, why: `not routable${m.trade.reason ? ` (${m.trade.reason})` : ""}` };
24
+ return { ok: true };
25
+ }
26
+ /** The listed creator of a launch: the token list's creator first, then the launch transaction's signer. */
27
+ export function followedOf(creators, l) {
28
+ for (const who of [l.creator, l.signer]) {
29
+ if (!who)
30
+ continue;
31
+ const c = creators.find((x) => x.address === who);
32
+ if (c)
33
+ return c;
34
+ }
35
+ return undefined;
36
+ }
37
+ export class CreatorTrigger {
38
+ name = "creator";
39
+ candidateLabel = "launches";
40
+ buyIdPrefix = "ccb";
41
+ sellIdPrefix = "ccs";
42
+ /** One decision at a time: a creator's caps are read from the ledger, and the booking of one buy lands before the next is decided. */
43
+ concurrency = 1;
44
+ d;
45
+ now;
46
+ state = new Map();
47
+ constructor(d) {
48
+ this.d = d;
49
+ this.now = d.now ?? Date.now;
50
+ }
51
+ async start(ctx) {
52
+ if (ctx?.wake)
53
+ this.d.source.onNew = () => ctx.wake();
54
+ await this.d.source.start();
55
+ }
56
+ stop() {
57
+ this.d.source.stop();
58
+ }
59
+ poll() {
60
+ const out = [];
61
+ for (const l of this.d.source.drain()) {
62
+ this.state.set(l.mint, { marketTried: 0 });
63
+ out.push({ mint: l.mint, chain: l.chain, seenAtMs: l.detectedAtMs, source: l.source, reason: "new launch", meta: l });
64
+ }
65
+ return out;
66
+ }
67
+ launchOf(c) {
68
+ return this.d.source.get(c.mint) ?? c.meta;
69
+ }
70
+ st(mint) {
71
+ let s = this.state.get(mint);
72
+ if (!s)
73
+ this.state.set(mint, (s = { marketTried: 0 }));
74
+ return s;
75
+ }
76
+ async decide(c, ctx, pending) {
77
+ const l = this.launchOf(c);
78
+ const s = this.st(c.mint);
79
+ const f = this.d.cfg.follow;
80
+ if (l.creator === this.d.cfg.wallet || l.signer === this.d.cfg.wallet)
81
+ return { verdict: "skip", reason: "own wallet's launch" };
82
+ const who = followedOf(f.creators, l);
83
+ if (!who) {
84
+ if (!l.creator) {
85
+ // A chain-first launch: Candle's list (which names the creator) may not have it yet. Wait until it ages out.
86
+ if (l.launchedAtMs === null)
87
+ return { verdict: "skip", reason: "launch time unknown" };
88
+ const ageSec = (this.now() - l.launchedAtMs) / 1000;
89
+ if (ageSec > f.maxAgeSec)
90
+ return { verdict: "skip", reason: `age ${Math.round(ageSec)}s > ${f.maxAgeSec}s` };
91
+ return { verdict: "wait", reason: "creator unknown" };
92
+ }
93
+ return { verdict: "skip", reason: "creator not followed" };
94
+ }
95
+ const lamports = lamportsOf(who.sol);
96
+ let v = ageAndMode(f, l, null, this.now());
97
+ if (v && v.verdict === "wait" && /mode unknown/.test(v.reason) && this.d.markets && s.marketTried < 10) {
98
+ s.marketTried++;
99
+ try {
100
+ const m = await this.readMarket(l.mint);
101
+ s.marketCheckedAt = this.now();
102
+ this.noteMarket(s, m);
103
+ if (m?.tier && !l.launchMode) {
104
+ l.launchMode = String(m.tier);
105
+ s.modeFromMarket = true;
106
+ }
107
+ if (typeof m?.decimals === "number" && l.decimals === undefined)
108
+ l.decimals = m.decimals;
109
+ }
110
+ catch {
111
+ /* not indexed yet */
112
+ }
113
+ v = ageAndMode(f, l, null, this.now());
114
+ }
115
+ let curve = null;
116
+ if (v === null || (v.verdict === "wait" && v.reason === "curve not readable yet")) {
117
+ const r = await ctx.readCurve(l.mint, l.curve ?? candleCurve(l.mint));
118
+ curve = r.status === "ok" ? r.curve : r.status === "missing" ? null : /owner|layout/.test(r.error) ? "unreadable" : null;
119
+ v = ageAndMode(f, l, curve, this.now());
120
+ }
121
+ if (v)
122
+ return v;
123
+ // The creator's caps (on top of the core's): from the ledger's own positions for this creator.
124
+ const used = this.d.ledger.creatorUsage(who.address, ctx.dry);
125
+ if (who.maxEntries !== undefined && used.entries >= who.maxEntries)
126
+ return { verdict: "skip", reason: `creator maxEntries ${who.maxEntries}` };
127
+ if (who.maxSol !== undefined && used.lamports + lamports > lamportsOf(who.maxSol))
128
+ return { verdict: "skip", reason: `creator maxSol ${who.maxSol}` };
129
+ const caps = ctx.caps(l.mint);
130
+ if (caps.paused)
131
+ return { verdict: "skip", reason: "PAUSE_BUYS" };
132
+ if (caps.entriesForMint >= this.d.cfg.buy.maxPerCoin)
133
+ return { verdict: "skip", reason: `maxPerCoin ${this.d.cfg.buy.maxPerCoin}` };
134
+ if (caps.open >= this.d.cfg.buy.maxOpen)
135
+ return { verdict: "skip", reason: `maxOpen ${this.d.cfg.buy.maxOpen}` };
136
+ if (caps.lifetimeLamports + lamports > lamportsOf(this.d.cfg.buy.lifetimeSol))
137
+ return { verdict: "skip", reason: `lifetimeSol ${this.d.cfg.buy.lifetimeSol}` };
138
+ const cv = onCurve(f, l, curve, lamports);
139
+ if (cv.verdict !== "buy")
140
+ return cv;
141
+ // The creator's own dev buy: the listed wallet holds the coin. Read at most every 2 s.
142
+ if (f.requireDevBuy && !s.devBuy) {
143
+ if (s.devCheckedAt === undefined || this.now() - s.devCheckedAt >= DEV_BUY_CHECK_MS) {
144
+ s.devCheckedAt = this.now();
145
+ s.devBuy = await this.devBought(l, who);
146
+ }
147
+ if (!s.devBuy)
148
+ return { verdict: "wait", reason: "no dev buy yet" };
149
+ }
150
+ // A launch whose mode Candle's list did not give is bought only once Candle's market record has the market.
151
+ if ((!l.launchMode || s.modeFromMarket) && !s.marketOk) {
152
+ if (this.d.markets && (s.marketCheckedAt === undefined || this.now() - s.marketCheckedAt >= MARKET_CHECK_MS)) {
153
+ s.marketCheckedAt = this.now();
154
+ try {
155
+ this.noteMarket(s, await this.readMarket(l.mint));
156
+ }
157
+ catch (e) {
158
+ s.marketWhy = `market read failed: ${String(e?.message ?? e).slice(0, 120)}`;
159
+ }
160
+ }
161
+ else if (!this.d.markets)
162
+ s.marketWhy = "no market source";
163
+ if (!s.marketOk)
164
+ return { verdict: "wait", reason: NO_CANDLE_MARKET };
165
+ }
166
+ s.impactBps = cv.impactBps;
167
+ return {
168
+ verdict: "buy",
169
+ reason: `${who.label ?? who.address.slice(0, 8)}: entry, impact ${cv.impactBps} bps`,
170
+ lamports,
171
+ deadlineAtMs: l.launchedAtMs === null ? null : l.launchedAtMs + f.maxAgeSec * 1000,
172
+ expectedOutRaw: cv.expectedOutRaw,
173
+ decimals: l.decimals,
174
+ // `followed` is what the creator's caps count by (Ledger.creatorUsage).
175
+ context: { ...l, followed: who.address, label: who.label ?? null },
176
+ log: (now) => ({ symbol: l.symbol, creator: who.address, label: who.label, sol: who.sol, impactBps: cv.impactBps, ageMs: l.launchedAtMs ? now - l.launchedAtMs : null, lastWait: pending.lastReason }),
177
+ };
178
+ }
179
+ /** The listed creator's own wallet holds the coin (not the launch's signer, which is often a payer wallet). */
180
+ async devBought(l, who) {
181
+ try {
182
+ const b = await tokenBalance(this.d.rpc, who.address, l.mint);
183
+ return b !== null && b > 0n;
184
+ }
185
+ catch {
186
+ return false; // unread: not proven
187
+ }
188
+ }
189
+ readMarket(mint) {
190
+ let timer;
191
+ return Promise.race([
192
+ this.d.markets.getMarket("solana", mint),
193
+ new Promise((_r, rej) => (timer = setTimeout(() => rej(new Error("market read timeout")), MARKET_READ_TIMEOUT_MS))),
194
+ ]).finally(() => clearTimeout(timer));
195
+ }
196
+ noteMarket(s, m) {
197
+ const has = candleHasMarket(m);
198
+ if (has.ok) {
199
+ s.marketOk = true;
200
+ s.marketWhy = undefined;
201
+ }
202
+ else
203
+ s.marketWhy = has.why;
204
+ }
205
+ onVerdict(c, v, _ctx, pending) {
206
+ const l = this.launchOf(c);
207
+ const who = followedOf(this.d.cfg.follow.creators, l);
208
+ if (v.verdict === "wait") {
209
+ if (who && v.reason !== pending.lastReason)
210
+ this.d.log.info("follow_wait", { mint: c.mint, creator: who.address, reason: v.reason });
211
+ return;
212
+ }
213
+ const s = this.state.get(c.mint);
214
+ this.state.delete(c.mint);
215
+ if (!who)
216
+ return; // launches of creators not on the list leave no row
217
+ this.d.ledger.recordFollow({
218
+ mint: c.mint, creator: who.address, label: who.label ?? null, launched_at: l.launchedAtMs, first_seen_at: l.detectedAtMs,
219
+ symbol: l.symbol ?? null, launch_mode: l.launchMode ?? null, decision: v.verdict, reason: v.verdict === "skip" ? v.reason : `impact ${s?.impactBps} bps`,
220
+ });
221
+ this.d.log.info(v.verdict === "skip" ? "follow_skip" : "follow_buy", { mint: c.mint, creator: who.address, label: who.label, symbol: l.symbol, ...(v.verdict === "skip" ? { reason: v.reason } : {}) });
222
+ }
223
+ status(pending) {
224
+ const dry = this.d.cfg.mode !== "live";
225
+ return {
226
+ follow: {
227
+ creators: this.d.cfg.follow.creators.map((c) => {
228
+ const u = this.d.ledger.creatorUsage(c.address, dry);
229
+ return { address: c.address, label: c.label ?? null, sol: c.sol, entries: u.entries, spentSol: Number(u.lamports) / 1e9, maxEntries: c.maxEntries ?? null, maxSol: c.maxSol ?? null };
230
+ }),
231
+ pending: pending.map((p) => ({ mint: p.candidate.mint, waiting: p.lastReason })),
232
+ recent: this.d.ledger.recentFollows(10),
233
+ source: this.d.source.stats ?? null,
234
+ },
235
+ };
236
+ }
237
+ }
238
+ /** The creator follower's engine: the core engine with the creator trigger. */
239
+ export class Engine extends CoreEngine {
240
+ constructor(d) {
241
+ super({ cfg: d.cfg, ledger: d.ledger, trigger: new CreatorTrigger(d), executor: d.executor, strategy: d.strategy, rpc: d.rpc, log: d.log, alerter: d.alerter, now: d.now });
242
+ }
243
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "comment": "Dry run: decides and logs, never trades. No API key needed. Put the creators you follow in follow.creators.",
3
+ "mode": "dry",
4
+ "payer": "embedded",
5
+ "apiKeyEnv": "CANDLE_API_KEY",
6
+ "rpcUrlEnv": "SOLANA_RPC_URL",
7
+ "wallet": "11111111111111111111111111111111",
8
+ "stateDir": "./state-dry",
9
+ "buy": { "sol": 0.01, "maxSlippageBps": 500, "maxOpen": 3, "lifetimeSol": 0.1, "maxPerCoin": 1 },
10
+ "sell": { "maxSlippageBps": 1000 },
11
+ "follow": {
12
+ "creators": [{ "address": "11111111111111111111111111111112", "label": "example", "sol": 0.005, "maxEntries": 3 }],
13
+ "maxAgeSec": 60,
14
+ "maxImpactBps": 300
15
+ },
16
+ "exit": { "strategy": "tpsl", "takeProfit": 2, "stopLoss": 0.5 }
17
+ }
package/package.json CHANGED
@@ -1,6 +1,62 @@
1
1
  {
2
2
  "name": "candle-creator-bot",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Buys the new Candle launches of the creators you list, with a ticket and caps per creator, and exits them by a pluggable strategy. A Solana bot built on @candledottv/agent-sdk.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/BruceJeet/candle-bots.git",
10
+ "directory": "packages/creator-bot"
11
+ },
12
+ "homepage": "https://github.com/BruceJeet/candle-bots/tree/main/packages/creator-bot#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/BruceJeet/candle-bots/issues"
15
+ },
16
+ "bin": {
17
+ "candle-creator-bot": "bin/candle-creator-bot"
18
+ },
19
+ "engines": {
20
+ "node": ">=22.18"
21
+ },
22
+ "files": [
23
+ "bin",
24
+ "dist",
25
+ "examples",
26
+ "README.md",
27
+ "CHANGELOG.md",
28
+ "LICENSE"
29
+ ],
30
+ "scripts": {
31
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
32
+ "typecheck": "tsc --noEmit -p .",
33
+ "prepack": "npm run build"
34
+ },
35
+ "dependencies": {
36
+ "candle-bot-core": "0.4.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "26.6.6",
40
+ "typescript": "7.0.2"
41
+ },
42
+ "keywords": [
43
+ "candle",
44
+ "solana",
45
+ "trading-bot",
46
+ "creator",
47
+ "copy",
48
+ "launch",
49
+ "memecoin",
50
+ "bot",
51
+ "agent"
52
+ ],
53
+ "main": "dist/index.js",
54
+ "types": "dist/index.d.ts",
55
+ "exports": {
56
+ ".": {
57
+ "types": "./dist/index.d.ts",
58
+ "default": "./dist/index.js"
59
+ },
60
+ "./package.json": "./package.json"
61
+ }
62
+ }