candle-event-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,14 @@
1
+ # Changelog: candle-event-bot
2
+
3
+ ## 0.1.0
4
+
5
+ First version, on candle-bot-core 0.6.0.
6
+
7
+ - `events`: a list of coins, each with a dated event (ISO 8601 with its offset) and a label.
8
+ - Buys each coin once (`buy.maxPerCoin`) in a window before its event (`entry.fromHours` to `entry.untilMin`), on its
9
+ own venue: the Candle curve, its DAMM v2 pool, the pump.fun curve or the canonical PumpSwap pool, quoted by that
10
+ venue's math. Refused venues (mayhem-mode, cashback, another quote mint, a venue it does not know) and an impact over
11
+ `entry.maxImpactBps` are skipped before any attempt. Retries are rechecked on the venue (impact and drift).
12
+ - The `event` exit (core): optional ladder rungs and a stop loss during the build-up, a hedge at the event (keep
13
+ `hedgeKeep`), a moonshot core after it (keep `coreKeep`), and the core's own take profit and max hold.
14
+ - `plan` prints each event's buy window and exit phases in clock time; never trades.
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,91 @@
1
- # Temporary Holding Version
1
+ # candle-event-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 coins tied to a dated event (a launch, a keynote, a listing) in a window before it, and exits them in phases
4
+ keyed to the event: ride the build-up, hedge at the event, keep a moonshot core. A Solana bot built on
5
+ [candle-bot-core](../core) and `@candledottv/agent-sdk`.
6
+
7
+ ```sh
8
+ npm install -g candle-event-bot
9
+ candle-event-bot plan --config examples/dry.json # each event's buy window and exit phases; never trades
10
+ candle-event-bot run --config examples/dry.json # dry run: decides and logs, never trades
11
+ ```
12
+
13
+ ## Events
14
+
15
+ ```json
16
+ "events": [{ "mint": "<the coin>", "at": "2026-12-01T17:00:00Z", "label": "the keynote" }]
17
+ ```
18
+
19
+ One event per coin. `at` is an ISO 8601 time **with its offset**; a time without one is refused, so it can never be
20
+ read in the machine's local time by mistake. Check the date yourself. A wrong day is the costliest mistake this bot
21
+ can make, and `plan` prints every phase in clock time to catch it.
22
+
23
+ ## When it buys
24
+
25
+ `entry`:
26
+
27
+ | key | default | meaning |
28
+ |---|---|---|
29
+ | `fromHours` | 72 | the buy window opens this long before the event |
30
+ | `untilMin` | 60 | and closes this long before it (no attempt is built after) |
31
+ | `maxImpactBps` | 300 | the ticket's impact on the venue (also the retry limit) |
32
+ | `buyWithinSec` | 60 | no attempt is built later than this after the decision |
33
+ | `retryMin` | 10 | a coin skipped in its window is looked at again after this long |
34
+ | `checkPrograms` / `extraPrograms` | true / [] | linked payer: only builds calling known programs are signed. With payer `main` none of these three checks applies (Candle signs inside the build) |
35
+ | `sellFloorMarginBps` | 200 | linked payer: a sell is never signed below the venue's expected out less `sell.maxSlippageBps` and this. pump.fun venues only: on a Candle curve or DAMM v2 coin, or when the read fails, only Candle's own floor applies |
36
+ | `buyFloorMarginBps` | 200 | linked payer: a buy is never signed with a floor under the decision fill less `buy.maxSlippageBps` and this (the first attempt too) |
37
+
38
+ Each coin is bought once (`buy.maxPerCoin`, default 1) with a ticket of `buy.sol`. The coin's venue is read fresh at
39
+ the decision: the Candle curve, its DAMM v2 pool, the pump.fun curve or the canonical PumpSwap pool. The buy must be
40
+ quoted by that venue's own math. These are skipped before any attempt:
41
+ - a venue the math refuses (mayhem-mode, cashback, another quote mint, a buy past the curve's tokens);
42
+ - a coin on a venue it does not know (e.g. a Raydium or Orca pool);
43
+ - an impact over the limit.
44
+
45
+ The engine keeps the smaller of the bot's expected fill and its own read. A retry is rechecked on the venue: the
46
+ ticket's impact, and the drift against the decision fill (`retry.maxPriceDriftBps`, else `maxImpactBps`). The core's
47
+ caps, `PAUSE_BUYS` and `STOP` apply as in every bot.
48
+
49
+ ## Exits
50
+
51
+ Any core strategy, or `event` (the phased exit; the bot gives it each coin's event time):
52
+
53
+ ```json
54
+ { "strategy": "event", "rungs": [{ "at": 3, "sell": 0.2 }], "stopLoss": 0.3,
55
+ "hedgeAtMin": 0, "hedgeKeep": 0.5, "afterMin": 60, "coreKeep": 0.2,
56
+ "coreTakeProfit": 20, "coreMaxHoldHours": 168 }
57
+ ```
58
+
59
+ | phase | when | what |
60
+ |---|---|---|
61
+ | build-up | until `hedgeAtMin` after the event | only `rungs` (the ladder's: at a multiple, sell that share of the original, in all) and `stopLoss` (a multiple: everything goes), each optional |
62
+ | hedge | from `hedgeAtMin` (default 0; negative is before the event) | keep at most `hedgeKeep` (default 0.5) of the original |
63
+ | core | from `afterMin` (default 60) | keep at most `coreKeep` (default 0.2): the moonshot core |
64
+ | core exit | any time after `afterMin` | `coreTakeProfit` (a multiple) or `coreMaxHoldHours` after the event: the core goes; without them it is held |
65
+
66
+ Every phase is a target for the share of the original position kept, so a restart, a failed sell or a late tick
67
+ never sells twice. Without `rungs` and `stopLoss` nothing sells before the hedge. `stopLoss` acts only in the build-up:
68
+ from the hedge on there is no stop loss, and what is kept rides the event down as well as up, until `coreTakeProfit`
69
+ or `coreMaxHoldHours`.
70
+
71
+ - **The buy window must close before the hedge:** a config whose window (`entry.untilMin`) reaches the hedge time
72
+ (`hedgeAtMin`) is refused at load, and a decision at or past the hedge is skipped. A buy then would be sold down at
73
+ once.
74
+ - **The phases follow the event time in the config as it is now.** An event moved while a coin is held moves its
75
+ phases. Moved into the past, the phases already due sell at the next tick. Edit an event's time only while flat in
76
+ that coin, unless moving its phases is what you want.
77
+ - **A coin held with no event in the list** (removed or changed in the config) gets no phase at all. The bot alerts
78
+ on it while it lasts. List it again, or `sell` it by hand.
79
+ - Event times are checked as real dates: Feb 30 or 24:00 are refused, not rolled over.
80
+
81
+ ## Known limits
82
+
83
+ - The venues are the four above. A coin that trades elsewhere is skipped, not guessed.
84
+ - The bot buys at the first look inside the window that passes its checks. It does not time the entry within the
85
+ window.
86
+ - Spend: `buy.lifetimeSol` counts tickets only. Candle's platform fee (0.25 % a side), network fees and about
87
+ 0.002 SOL of token-account rent per new coin come on top.
88
+
89
+ ## License
90
+
91
+ 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,22 @@
1
+ import { Engine } from "candle-bot-core/engine";
2
+ import { Ledger } from "candle-bot-core/ledger";
3
+ import { type Ctx as CoreCtx } from "candle-bot-core/runtime";
4
+ import { type Config } from "./config.ts";
5
+ import { EventTrigger } from "./trigger.ts";
6
+ export declare const HELP = "candle-event-bot: buys coins tied to a dated event (a launch, a keynote, a listing) in a window before it, and exits\nthem in phases keyed to the event: ride the build-up, hedge at the event, keep a moonshot core.\n\n candle-event-bot run --config bot.json [--mode dry|live] run the bot\n candle-event-bot plan --config bot.json print each event's buy window and exit phases; never trades\n candle-event-bot status --config bot.json print status.json\n candle-event-bot sell <mint> --config bot.json [--fraction 1]\n\nStart from examples/dry.json. Controls: create STOP or PAUSE_BUYS in the state folder. Docs: the README.\n";
7
+ type Ctx = CoreCtx<Config>;
8
+ /**
9
+ * The engine: buys checked on their own venue (Candle curve, DAMM v2 pool, pump.fun curve, PumpSwap pool) by the
10
+ * engine and the executor's retry recheck; the `event` exit gets each coin's event time.
11
+ */
12
+ export declare function makeEngine(ctx: Ctx, ledger: Ledger, d?: {
13
+ now?: () => number;
14
+ }): Promise<{
15
+ engine: Engine;
16
+ trigger: EventTrigger;
17
+ }>;
18
+ /** Each event's buy window and, for the event exit, its phases in clock time. */
19
+ export declare function plan(cfg: Config, now?: number): Array<Record<string, unknown>>;
20
+ /** The entry point: never lets an error out unredacted (the SDK puts RPC URLs in its messages). */
21
+ export declare function main(argv?: string[]): Promise<number>;
22
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,110 @@
1
+ /** The command line: run, plan, status, sell. */
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { Engine } from "candle-bot-core/engine";
5
+ import { Executor } from "candle-bot-core/exec/executor";
6
+ import { KNOWN_PROGRAMS } from "candle-bot-core/exec/trader";
7
+ import { Ledger } from "candle-bot-core/ledger";
8
+ import { sellFloor } from "candle-bot-core/market/reader";
9
+ import { args, context as coreContext, die, guardMain, parts, run, sell, status } from "candle-bot-core/runtime";
10
+ import { loadConfig, ConfigError, eventTimes } from "./config.js";
11
+ import { EventTrigger, windowOf } from "./trigger.js";
12
+ import { EVENT_DEFAULTS } from "candle-bot-core/strategies/event";
13
+ export const HELP = `candle-event-bot: buys coins tied to a dated event (a launch, a keynote, a listing) in a window before it, and exits
14
+ them in phases keyed to the event: ride the build-up, hedge at the event, keep a moonshot core.
15
+
16
+ candle-event-bot run --config bot.json [--mode dry|live] run the bot
17
+ candle-event-bot plan --config bot.json print each event's buy window and exit phases; never trades
18
+ candle-event-bot status --config bot.json print status.json
19
+ candle-event-bot sell <mint> --config bot.json [--fraction 1]
20
+
21
+ Start from examples/dry.json. Controls: create STOP or PAUSE_BUYS in the state folder. Docs: the README.
22
+ `;
23
+ function context(configPath, modeOverride) {
24
+ let cfg;
25
+ try {
26
+ cfg = loadConfig(configPath);
27
+ }
28
+ catch (e) {
29
+ if (e instanceof ConfigError)
30
+ die(e.message);
31
+ throw e;
32
+ }
33
+ return coreContext(cfg, modeOverride);
34
+ }
35
+ /**
36
+ * The engine: buys checked on their own venue (Candle curve, DAMM v2 pool, pump.fun curve, PumpSwap pool) by the
37
+ * engine and the executor's retry recheck; the `event` exit gets each coin's event time.
38
+ */
39
+ export async function makeEngine(ctx, ledger, d = {}) {
40
+ const c = ctx.cfg;
41
+ const linked = c.payer === "linked";
42
+ const known = linked && c.entry.checkPrograms ? new Set([...KNOWN_PROGRAMS, ...c.entry.extraPrograms]) : undefined;
43
+ const floor = linked ? sellFloor(ctx.rpc, c.sell.maxSlippageBps, c.entry.sellFloorMarginBps) : undefined;
44
+ const { strategy, alerter, executor } = await parts(ctx, ledger, {
45
+ maxBuyImpactBps: c.entry.maxImpactBps,
46
+ executor: (x) => new Executor({ ...x, dammBuys: true, pumpBuys: true, ...(known ? { knownPrograms: known } : {}), ...(floor ? { sellFloor: floor, buyFloorMarginBps: c.entry.buyFloorMarginBps } : {}) }),
47
+ venues: { pump: true },
48
+ ...(c.exit.strategy === "event" ? { exitParams: { events: eventTimes(c) } } : {}),
49
+ ...(d.now ? { now: d.now } : {}),
50
+ });
51
+ const trigger = new EventTrigger({ cfg: c, log: ctx.log, ...(d.now ? { now: d.now } : {}) });
52
+ const engine = new Engine({ cfg: c, ledger, trigger, executor, strategy, rpc: ctx.rpc, log: ctx.log, alerter, pumpBuys: true, venues: { pump: true }, ...(d.now ? { now: d.now } : {}) });
53
+ return { engine, trigger };
54
+ }
55
+ const openLedger = (ctx) => () => new Ledger(join(ctx.cfg.stateDir, "ledger.sqlite"));
56
+ /** Each event's buy window and, for the event exit, its phases in clock time. */
57
+ export function plan(cfg, now = Date.now()) {
58
+ const x = cfg.exit;
59
+ return cfg.events.map((e) => {
60
+ const w = windowOf(cfg, e);
61
+ const iso = (t) => new Date(t).toISOString();
62
+ const row = { mint: e.mint, label: e.label, event: iso(e.atMs), buyFrom: iso(w.opens), buyUntil: iso(w.closes), window: now < w.opens ? "not open" : now >= w.closes ? "closed" : "open" };
63
+ if (x.strategy === "event") {
64
+ const v = { ...EVENT_DEFAULTS, ...x };
65
+ row.hedge = `${iso(e.atMs + v.hedgeAtMin * 60_000)}: keep ${(v.hedgeKeep * 100).toFixed(0)} %`;
66
+ row.core = `${iso(e.atMs + v.afterMin * 60_000)}: keep ${(v.coreKeep * 100).toFixed(0)} %`;
67
+ if (x.coreMaxHoldHours !== undefined)
68
+ row.coreEnds = iso(e.atMs + x.coreMaxHoldHours * 3_600_000);
69
+ }
70
+ return row;
71
+ });
72
+ }
73
+ /** The entry point: never lets an error out unredacted (the SDK puts RPC URLs in its messages). */
74
+ export async function main(argv = process.argv.slice(2)) {
75
+ return guardMain(async () => {
76
+ const { pos, opt } = args(argv);
77
+ const cmd = pos[0];
78
+ if (!cmd || cmd === "help" || opt.help) {
79
+ process.stdout.write(HELP);
80
+ return cmd || opt.help ? 0 : 2;
81
+ }
82
+ if (cmd === "version" || opt.version) {
83
+ process.stdout.write(JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version + "\n");
84
+ return 0;
85
+ }
86
+ const config = opt.config ?? "bot.json";
87
+ switch (cmd) {
88
+ case "run": {
89
+ const ctx = context(config, opt.mode);
90
+ return run(ctx, openLedger(ctx), (ledger) => makeEngine(ctx, ledger));
91
+ }
92
+ case "plan": {
93
+ const ctx = context(config, "dry");
94
+ for (const r of plan(ctx.cfg))
95
+ process.stdout.write(JSON.stringify(r) + "\n");
96
+ return 0;
97
+ }
98
+ case "status":
99
+ return status(context(config));
100
+ case "sell": {
101
+ if (!pos[1])
102
+ die("sell <mint> [--fraction f]");
103
+ const ctx = context(config);
104
+ return sell(ctx, pos[1], opt.fraction ? Number(opt.fraction) : 1, openLedger(ctx), (ledger) => makeEngine(ctx, ledger));
105
+ }
106
+ default:
107
+ die(`unknown command ${cmd}`);
108
+ }
109
+ });
110
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The one config file: the core's shared sections (candle-bot-core) plus `events` (the dated events: a coin and a
3
+ * time each) and `entry` (when, before each event, the coin is bought, and the limits). The exit is any core strategy;
4
+ * `event` (the phased exit) gets each coin's event time from `events` (a config never sets exit.events itself).
5
+ * Unknown keys are refused at every level.
6
+ */
7
+ import { type Config as CoreConfig } from "candle-bot-core/config";
8
+ export { ConfigError } from "candle-bot-core/config";
9
+ export interface EventItem {
10
+ mint: string;
11
+ /** The event time (ms), from an ISO 8601 time with its offset (e.g. "2026-12-01T17:00:00Z"). */
12
+ atMs: number;
13
+ label: string;
14
+ }
15
+ export interface Entry {
16
+ /** The buy window opens this many hours before the event... */
17
+ fromHours: number;
18
+ /** ...and closes this many minutes before it (a buy is never built after). */
19
+ untilMin: number;
20
+ /** The ticket's price impact on the venue, bps (the fee apart). Also the buy-retry limit. */
21
+ maxImpactBps: number;
22
+ /** No attempt of a buy is built more than this many seconds after its decision (nor after the window closes). */
23
+ buyWithinSec: number;
24
+ /** A coin skipped in its window is looked at again after this long (min). */
25
+ retryMin: number;
26
+ /** Linked payer: sign only builds whose programs are known (the core's list plus extraPrograms). Default true. */
27
+ checkPrograms: boolean;
28
+ extraPrograms: string[];
29
+ /**
30
+ * Linked payer: a sell is never signed below the venue's own expected out less sell.maxSlippageBps and this (pump.fun
31
+ * venues only; on another venue, or when the read fails, only Candle's own floor applies).
32
+ */
33
+ sellFloorMarginBps: number;
34
+ /** Linked payer: a buy is never signed with a floor under the decision fill less buy.maxSlippageBps and this. */
35
+ buyFloorMarginBps: number;
36
+ }
37
+ export type Config = CoreConfig & {
38
+ events: EventItem[];
39
+ entry: Entry;
40
+ };
41
+ declare function parseEvents(raw: unknown): EventItem[];
42
+ declare function parseEntry(raw: unknown): Entry;
43
+ export declare const SECTIONS: {
44
+ events: typeof parseEvents;
45
+ entry: typeof parseEntry;
46
+ };
47
+ /** The exit's event times, from the event list (the `event` strategy reads them). */
48
+ export declare function eventTimes(c: Pick<Config, "events">): Record<string, number>;
49
+ export declare function parseConfig(raw: unknown, baseDir?: string): Config;
50
+ export declare function loadConfig(path: string): Config;
package/dist/config.js ADDED
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The one config file: the core's shared sections (candle-bot-core) plus `events` (the dated events: a coin and a
3
+ * time each) and `entry` (when, before each event, the coin is bought, and the limits). The exit is any core strategy;
4
+ * `event` (the phased exit) gets each coin's event time from `events` (a config never sets exit.events itself).
5
+ * Unknown keys are refused at every level.
6
+ */
7
+ import { parseConfig as parseCore, loadConfig as loadCore, ConfigError, isObj, only, num, bool, str, strList } from "candle-bot-core/config";
8
+ import { asAddress } from "candle-bot-core/solana/base58";
9
+ import { create as eventExit, EVENT_DEFAULTS } from "candle-bot-core/strategies/event";
10
+ export { ConfigError } from "candle-bot-core/config";
11
+ function parseEvents(raw) {
12
+ const w = "config.events";
13
+ if (!Array.isArray(raw) || raw.length === 0)
14
+ throw new ConfigError(`${w}: a non-empty list of { mint, at, label? }`);
15
+ if (raw.length > 200)
16
+ throw new ConfigError(`${w}: at most 200 events`);
17
+ const seen = new Set();
18
+ return raw.map((e, i) => {
19
+ const k = `${w}[${i}]`;
20
+ if (!isObj(e))
21
+ throw new ConfigError(`${k}: { mint, at, label? }`);
22
+ only(e, ["mint", "at", "label"], k);
23
+ const mint = str(e, "mint", k);
24
+ if (asAddress(mint) !== mint)
25
+ throw new ConfigError(`${k}.mint: "${mint}" is not a Solana address`);
26
+ const at = str(e, "at", k);
27
+ // An explicit offset is required: a time without one would read as the machine's local time.
28
+ const g = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?(Z|[+-]\d{2}:\d{2})$/.exec(at);
29
+ if (!g)
30
+ throw new ConfigError(`${k}.at: an ISO 8601 time with its offset, e.g. "2026-12-01T17:00:00Z"`);
31
+ // A date that does not exist (Feb 30, 24:00) would roll over to another day without a word: refused.
32
+ const [y, mo, d, hh, mm, ss] = [g[1], g[2], g[3], g[4], g[5], g[6] ?? "0"].map(Number);
33
+ if (mo < 1 || mo > 12 || d < 1 || d > new Date(Date.UTC(y, mo, 0)).getUTCDate() || hh > 23 || mm > 59 || ss > 59)
34
+ throw new ConfigError(`${k}.at: "${at}" is not a real date and time`);
35
+ const atMs = Date.parse(at);
36
+ if (!Number.isFinite(atMs))
37
+ throw new ConfigError(`${k}.at: not a time`);
38
+ if (seen.has(mint))
39
+ throw new ConfigError(`${k}.mint: ${mint} is listed twice (one event per coin)`);
40
+ seen.add(mint);
41
+ return { mint, atMs, label: str(e, "label", k, { def: "" }) };
42
+ });
43
+ }
44
+ function parseEntry(raw) {
45
+ const w = "config.entry";
46
+ const d = raw === undefined ? {} : raw;
47
+ if (!isObj(d))
48
+ throw new ConfigError(`${w}: must be an object`);
49
+ only(d, ["fromHours", "untilMin", "maxImpactBps", "buyWithinSec", "retryMin", "checkPrograms", "extraPrograms", "sellFloorMarginBps", "buyFloorMarginBps"], w);
50
+ const out = {
51
+ fromHours: num(d, "fromHours", w, { min: 0.1, max: 8760, def: 72 }),
52
+ untilMin: num(d, "untilMin", w, { min: -10_080, max: 525_600, def: 60 }),
53
+ maxImpactBps: num(d, "maxImpactBps", w, { min: 0, max: 10_000, def: 300 }),
54
+ buyWithinSec: num(d, "buyWithinSec", w, { min: 5, max: 3600, int: true, def: 60 }),
55
+ retryMin: num(d, "retryMin", w, { min: 1, max: 1440, int: true, def: 10 }),
56
+ checkPrograms: bool(d, "checkPrograms", w, true),
57
+ extraPrograms: strList(d, "extraPrograms", w, { def: [], address: true }),
58
+ sellFloorMarginBps: num(d, "sellFloorMarginBps", w, { min: 0, max: 10_000, int: true, def: 200 }),
59
+ buyFloorMarginBps: num(d, "buyFloorMarginBps", w, { min: 0, max: 10_000, int: true, def: 200 }),
60
+ };
61
+ if (out.untilMin >= out.fromHours * 60)
62
+ throw new ConfigError(`${w}: the window closes (untilMin before the event) before it opens (fromHours before it)`);
63
+ return out;
64
+ }
65
+ export const SECTIONS = { events: parseEvents, entry: parseEntry };
66
+ /** The exit's event times, from the event list (the `event` strategy reads them). */
67
+ export function eventTimes(c) {
68
+ return Object.fromEntries(c.events.map((e) => [e.mint, e.atMs]));
69
+ }
70
+ /** The event exit is checked at load, with the event times filled in (a config never sets exit.events). */
71
+ function checked(c) {
72
+ if (c.exit.strategy === "event") {
73
+ if (c.exit.events !== undefined)
74
+ throw new ConfigError("config.exit.events: filled from config.events; leave it out");
75
+ // The buy window must close before the hedge: a buy after it would be sold down at the next exit tick.
76
+ const hedgeAtMin = typeof c.exit.hedgeAtMin === "number" ? c.exit.hedgeAtMin : EVENT_DEFAULTS.hedgeAtMin;
77
+ if (-c.entry.untilMin >= hedgeAtMin)
78
+ throw new ConfigError(`config.entry.untilMin: the buy window closes ${-c.entry.untilMin} min after the event, at or after the hedge (exit.hedgeAtMin ${hedgeAtMin}): a buy then would be sold at once`);
79
+ try {
80
+ const { strategy: _s, ...params } = c.exit;
81
+ eventExit({ ...params, events: eventTimes(c) });
82
+ }
83
+ catch (e) {
84
+ throw new ConfigError(String(e.message));
85
+ }
86
+ }
87
+ return c;
88
+ }
89
+ export function parseConfig(raw, baseDir = process.cwd()) {
90
+ return checked(parseCore(raw, baseDir, SECTIONS));
91
+ }
92
+ export function loadConfig(path) {
93
+ return checked(loadCore(path, SECTIONS));
94
+ }
@@ -0,0 +1,6 @@
1
+ /** candle-event-bot: the trigger, the config and the wiring, for use as a library. */
2
+ export { EventTrigger, windowOf } from "./trigger.ts";
3
+ export type { TriggerDeps } from "./trigger.ts";
4
+ export { parseConfig, loadConfig, eventTimes, SECTIONS, ConfigError } from "./config.ts";
5
+ export type { Config, EventItem, Entry } from "./config.ts";
6
+ export { main, HELP, makeEngine, plan } from "./cli.ts";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ /** candle-event-bot: the trigger, the config and the wiring, for use as a library. */
2
+ export { EventTrigger, windowOf } from "./trigger.js";
3
+ export { parseConfig, loadConfig, eventTimes, SECTIONS, ConfigError } from "./config.js";
4
+ export { main, HELP, makeEngine, plan } from "./cli.js";
@@ -0,0 +1,38 @@
1
+ import type { Logger } from "candle-bot-core/log";
2
+ import type { Candidate, PendingInfo, Trigger, TriggerContext, Verdict } from "candle-bot-core/trigger";
3
+ import type { Config, EventItem } from "./config.ts";
4
+ export interface TriggerDeps {
5
+ cfg: Config;
6
+ log: Logger;
7
+ now?: () => number;
8
+ }
9
+ /** The buy window of an event: [opens, closes) in ms. */
10
+ export declare function windowOf(cfg: Config, e: EventItem): {
11
+ opens: number;
12
+ closes: number;
13
+ };
14
+ export declare class EventTrigger implements Trigger {
15
+ readonly name = "event";
16
+ readonly candidateLabel = "events";
17
+ readonly buyIdPrefix = "ceb";
18
+ readonly sellIdPrefix = "ces";
19
+ private d;
20
+ private now;
21
+ private nextLook;
22
+ private decimals;
23
+ last: Array<{
24
+ mint: string;
25
+ label: string;
26
+ at: number;
27
+ decision: string;
28
+ reason: string;
29
+ }>;
30
+ constructor(d: TriggerDeps);
31
+ /** With the event exit: when, after the event, the hedge begins (a buy is never made from then on). */
32
+ private hedgeAtMs;
33
+ poll(ctx: TriggerContext): Candidate[];
34
+ private decimalsOf;
35
+ decide(c: Candidate, ctx: TriggerContext): Promise<Verdict>;
36
+ onVerdict(c: Candidate, v: Verdict, _ctx: TriggerContext, _p: PendingInfo): void;
37
+ status(): Record<string, unknown>;
38
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * The event bot's trigger: each listed coin is a candidate while its buy window is open (from `entry.fromHours` before
3
+ * its event to `entry.untilMin` before it) and the bot has not bought it (the engine's maxPerCoin). decide() reads the
4
+ * coin's venue fresh: the Candle curve, its DAMM v2 pool, the pump.fun curve or the canonical PumpSwap pool, quoted by
5
+ * that venue's own math (buyQuote). A venue the math refuses (mayhem-mode, cashback, another quote mint, a buy past
6
+ * the curve's tokens, a venue it does not know) is skipped before any attempt; so is an impact over `maxImpactBps`.
7
+ * The verdict's expected fill is the venue's quote, with a deadline: `buyWithinSec`, never past the window.
8
+ */
9
+ import { lamportsOf } from "candle-bot-core/engine";
10
+ import { decodeMint } from "candle-bot-core/market/mint";
11
+ import { buyQuote, readMarkets } from "candle-bot-core/market/reader";
12
+ import { EVENT_DEFAULTS } from "candle-bot-core/strategies/event";
13
+ /** The buy window of an event: [opens, closes) in ms. */
14
+ export function windowOf(cfg, e) {
15
+ return { opens: e.atMs - cfg.entry.fromHours * 3_600_000, closes: e.atMs - cfg.entry.untilMin * 60_000 };
16
+ }
17
+ export class EventTrigger {
18
+ name = "event";
19
+ candidateLabel = "events";
20
+ buyIdPrefix = "ceb";
21
+ sellIdPrefix = "ces";
22
+ d;
23
+ now;
24
+ nextLook = new Map();
25
+ decimals = new Map();
26
+ last = [];
27
+ constructor(d) {
28
+ this.d = d;
29
+ this.now = d.now ?? Date.now;
30
+ }
31
+ /** With the event exit: when, after the event, the hedge begins (a buy is never made from then on). */
32
+ hedgeAtMs() {
33
+ const x = this.d.cfg.exit;
34
+ if (x.strategy !== "event")
35
+ return null;
36
+ return (typeof x.hedgeAtMin === "number" ? x.hedgeAtMin : EVENT_DEFAULTS.hedgeAtMin) * 60_000;
37
+ }
38
+ poll(ctx) {
39
+ const now = this.now();
40
+ const out = [];
41
+ // A coin held with no event in the list (removed or changed in the config) gets no phase at all: said, while it lasts.
42
+ const listed = new Set(this.d.cfg.events.map((e) => e.mint));
43
+ for (const p of ctx.ledger.positions("opening", "open", "exit_stuck")) {
44
+ if (listed.has(p.mint))
45
+ continue;
46
+ ctx.alerter.raise(`event:no_event:${p.mint}`, `event bot: position ${p.id} ${p.mint} has no event in config.events, so no exit phase applies to it (held until it is listed again or sold by hand)`);
47
+ }
48
+ for (const e of this.d.cfg.events) {
49
+ const w = windowOf(this.d.cfg, e);
50
+ if (now < w.opens || now >= w.closes)
51
+ continue;
52
+ if ((this.nextLook.get(e.mint) ?? 0) > now)
53
+ continue;
54
+ if (ctx.caps(e.mint).entriesForMint >= this.d.cfg.buy.maxPerCoin)
55
+ continue;
56
+ this.nextLook.set(e.mint, now + this.d.cfg.entry.retryMin * 60_000);
57
+ out.push({ mint: e.mint, chain: "solana", seenAtMs: now, source: "event", reason: `${e.label || e.mint} in ${Math.round((e.atMs - now) / 60_000)} min`, meta: { label: e.label, eventAt: new Date(e.atMs).toISOString() } });
58
+ }
59
+ return out;
60
+ }
61
+ async decimalsOf(mint, ctx) {
62
+ const have = this.decimals.get(mint);
63
+ if (have !== undefined)
64
+ return have;
65
+ let d = 6;
66
+ try {
67
+ d = decodeMint((await ctx.rpc.call("getAccountInfo", [mint, { encoding: "base64", commitment: "confirmed" }]))?.value).decimals;
68
+ this.decimals.set(mint, d);
69
+ }
70
+ catch {
71
+ /* decimals only scale prices */
72
+ }
73
+ return d;
74
+ }
75
+ async decide(c, ctx) {
76
+ const e = this.d.cfg.events.find((x) => x.mint === c.mint);
77
+ if (!e)
78
+ return { verdict: "skip", reason: "not an event coin" };
79
+ const now = this.now();
80
+ const w = windowOf(this.d.cfg, e);
81
+ if (now < w.opens || now >= w.closes)
82
+ return { verdict: "skip", reason: "outside its buy window" };
83
+ const hedge = this.hedgeAtMs();
84
+ if (hedge !== null && now >= e.atMs + hedge)
85
+ return { verdict: "skip", reason: "past the event's hedge" };
86
+ const caps = ctx.caps(c.mint);
87
+ if (caps.paused)
88
+ return { verdict: "skip", reason: "PAUSE_BUYS" };
89
+ if (caps.entriesForMint >= ctx.cfg.buy.maxPerCoin)
90
+ return { verdict: "skip", reason: `maxPerCoin ${ctx.cfg.buy.maxPerCoin}` };
91
+ if (caps.open >= ctx.cfg.buy.maxOpen)
92
+ return { verdict: "skip", reason: `maxOpen ${ctx.cfg.buy.maxOpen}` };
93
+ const ticket = lamportsOf(ctx.cfg.buy.sol);
94
+ if (caps.lifetimeLamports + ticket > lamportsOf(ctx.cfg.buy.lifetimeSol))
95
+ return { verdict: "skip", reason: `lifetimeSol ${ctx.cfg.buy.lifetimeSol}` };
96
+ const decimals = await this.decimalsOf(c.mint, ctx);
97
+ let m = null;
98
+ try {
99
+ m = (await readMarkets(ctx.rpc, [{ mint: c.mint, decimals }], now, { pump: true })).get(c.mint) ?? null;
100
+ }
101
+ catch {
102
+ m = null;
103
+ }
104
+ if (!m)
105
+ return { verdict: "skip", reason: "venue not readable" };
106
+ const q = buyQuote(m, ticket);
107
+ if ("why" in q)
108
+ return { verdict: "skip", reason: `venue refused: ${q.why}` };
109
+ if (q.impactBps > this.d.cfg.entry.maxImpactBps)
110
+ return { verdict: "skip", reason: `impact ${q.impactBps} bps > ${this.d.cfg.entry.maxImpactBps}` };
111
+ return {
112
+ verdict: "buy",
113
+ reason: `${e.label || "event"} at ${new Date(e.atMs).toISOString()}; impact ${q.impactBps} bps on ${q.venue}`,
114
+ lamports: ticket,
115
+ deadlineAtMs: Math.min(now + this.d.cfg.entry.buyWithinSec * 1000, w.closes),
116
+ expectedOutRaw: q.expectedOutRaw,
117
+ decimals,
118
+ context: { label: e.label, eventAt: new Date(e.atMs).toISOString(), venue: q.venue, impactBps: q.impactBps },
119
+ log: { label: e.label, venue: q.venue, impactBps: q.impactBps, eventInMin: Math.round((e.atMs - now) / 60_000) },
120
+ };
121
+ }
122
+ onVerdict(c, v, _ctx, _p) {
123
+ if (v.verdict === "wait")
124
+ return;
125
+ const e = this.d.cfg.events.find((x) => x.mint === c.mint);
126
+ if (v.verdict === "skip")
127
+ this.d.log.info("event_skip", { mint: c.mint, label: e?.label, reason: v.reason });
128
+ this.last.push({ mint: c.mint, label: e?.label ?? "", at: this.now(), decision: v.verdict, reason: v.reason });
129
+ if (this.last.length > 20)
130
+ this.last.shift();
131
+ }
132
+ status() {
133
+ const now = this.now();
134
+ return {
135
+ events: this.d.cfg.events.map((e) => {
136
+ const w = windowOf(this.d.cfg, e);
137
+ return { mint: e.mint, label: e.label, at: new Date(e.atMs).toISOString(), window: now < w.opens ? "not open" : now >= w.closes ? "closed" : "open", inMin: Math.round((e.atMs - now) / 60_000) };
138
+ }),
139
+ recent: this.last,
140
+ };
141
+ }
142
+ }
@@ -0,0 +1,48 @@
1
+ {
2
+ "comment": "Dry run: decides and logs, never trades. No API key needed. Replace the event's mint with your coin and its time with the event's (ISO 8601 with its offset). Set your wallet (and a key for live) before going live.",
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": {
10
+ "sol": 0.01,
11
+ "maxSlippageBps": 500,
12
+ "maxOpen": 3,
13
+ "lifetimeSol": 0.1,
14
+ "maxPerCoin": 1
15
+ },
16
+ "sell": {
17
+ "maxSlippageBps": 1000
18
+ },
19
+ "events": [
20
+ {
21
+ "mint": "11111111111111111111111111111111",
22
+ "at": "2026-12-01T17:00:00Z",
23
+ "label": "replace me: the coin and its event"
24
+ }
25
+ ],
26
+ "entry": {
27
+ "fromHours": 72,
28
+ "untilMin": 60,
29
+ "maxImpactBps": 300,
30
+ "buyWithinSec": 60,
31
+ "retryMin": 10
32
+ },
33
+ "exit": {
34
+ "strategy": "event",
35
+ "rungs": [
36
+ {
37
+ "at": 3,
38
+ "sell": 0.2
39
+ }
40
+ ],
41
+ "hedgeAtMin": 0,
42
+ "hedgeKeep": 0.5,
43
+ "afterMin": 60,
44
+ "coreKeep": 0.2,
45
+ "coreTakeProfit": 20,
46
+ "coreMaxHoldHours": 168
47
+ }
48
+ }
package/package.json CHANGED
@@ -1,6 +1,61 @@
1
1
  {
2
2
  "name": "candle-event-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 coins tied to a dated event in a window before it, and exits them in phases keyed to the event: ride the build-up, hedge at the event, keep a moonshot core. 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/event-bot"
11
+ },
12
+ "homepage": "https://github.com/BruceJeet/candle-bots/tree/main/packages/event-bot#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/BruceJeet/candle-bots/issues"
15
+ },
16
+ "bin": {
17
+ "candle-event-bot": "bin/candle-event-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.6.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
+ "event",
47
+ "launch",
48
+ "memecoin",
49
+ "bot",
50
+ "agent"
51
+ ],
52
+ "main": "dist/index.js",
53
+ "types": "dist/index.d.ts",
54
+ "exports": {
55
+ ".": {
56
+ "types": "./dist/index.d.ts",
57
+ "default": "./dist/index.js"
58
+ },
59
+ "./package.json": "./package.json"
60
+ }
61
+ }