candle-launch-bot 0.0.0-stage → 0.1.1
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 +19 -0
- package/LICENSE +21 -0
- package/README.md +400 -2
- package/bin/candle-launch-bot +7 -0
- package/dist/alerts.d.ts +28 -0
- package/dist/alerts.js +82 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +321 -0
- package/dist/config.d.ts +88 -0
- package/dist/config.js +244 -0
- package/dist/detect/detector.d.ts +111 -0
- package/dist/detect/detector.js +348 -0
- package/dist/engine.d.ts +103 -0
- package/dist/engine.js +585 -0
- package/dist/exec/executor.d.ts +149 -0
- package/dist/exec/executor.js +859 -0
- package/dist/exec/fills.d.ts +25 -0
- package/dist/exec/fills.js +53 -0
- package/dist/exec/trader.d.ts +109 -0
- package/dist/exec/trader.js +310 -0
- package/dist/filters.d.ts +26 -0
- package/dist/filters.js +92 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +2 -0
- package/dist/init.d.ts +23 -0
- package/dist/init.js +192 -0
- package/dist/ledger.d.ts +189 -0
- package/dist/ledger.js +243 -0
- package/dist/log.d.ts +22 -0
- package/dist/log.js +40 -0
- package/dist/market/curve.d.ts +80 -0
- package/dist/market/curve.js +168 -0
- package/dist/market/reader.d.ts +37 -0
- package/dist/market/reader.js +106 -0
- package/dist/redact.d.ts +20 -0
- package/dist/redact.js +111 -0
- package/dist/solana/base58.d.ts +4 -0
- package/dist/solana/base58.js +45 -0
- package/dist/solana/logs-ws.d.ts +25 -0
- package/dist/solana/logs-ws.js +116 -0
- package/dist/solana/pda.d.ts +12 -0
- package/dist/solana/pda.js +70 -0
- package/dist/solana/rpc.d.ts +29 -0
- package/dist/solana/rpc.js +96 -0
- package/dist/strategies/hold.d.ts +3 -0
- package/dist/strategies/hold.js +6 -0
- package/dist/strategies/ladder.d.ts +8 -0
- package/dist/strategies/ladder.js +51 -0
- package/dist/strategies/load.d.ts +5 -0
- package/dist/strategies/load.js +44 -0
- package/dist/strategies/preGrad.d.ts +10 -0
- package/dist/strategies/preGrad.js +32 -0
- package/dist/strategies/time.d.ts +6 -0
- package/dist/strategies/time.js +16 -0
- package/dist/strategies/tpsl.d.ts +8 -0
- package/dist/strategies/tpsl.js +30 -0
- package/dist/strategies/types.d.ts +55 -0
- package/dist/strategies/types.js +31 -0
- package/examples/dry.json +32 -0
- package/examples/live-embedded.json +14 -0
- package/examples/live-linked.json +15 -0
- package/examples/strategies/breakeven-stop.js +22 -0
- package/examples/strategies/stale-coin.js +18 -0
- package/examples/strategies/timeboxed-tp.js +18 -0
- package/package.json +60 -4
- package/skills/candle-launch-bot/SKILL.md +71 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
- README rewrite: quick start up top, house rules.
|
|
6
|
+
- Repository links in `package.json`.
|
|
7
|
+
|
|
8
|
+
## 0.1.0
|
|
9
|
+
|
|
10
|
+
First release.
|
|
11
|
+
|
|
12
|
+
- Detection from Candle's token list and the launch-curve program on chain, with latency for both.
|
|
13
|
+
- Filters: launch mode, age, price impact, creator allow/deny, name/symbol match; caps for ticket, open positions,
|
|
14
|
+
lifetime spend and entries per coin.
|
|
15
|
+
- Payers `embedded` (Candle signs) and `linked` (TEE wallet, local key signer).
|
|
16
|
+
- Exits `hold`, `tpsl`, `time`, `ladder`, `preGrad`, and custom strategy files; three example strategies.
|
|
17
|
+
- Fills booked from the chain; sell clamp to the wallet balance; capped retries with backoff; recovery of trades that
|
|
18
|
+
may have executed; alerts to a command or a webhook.
|
|
19
|
+
- `init` (interactive or from flags), `scan`, `run`, `status`, `sell`; STOP and PAUSE_BUYS controls; `status.json`.
|
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,401 @@
|
|
|
1
|
-
#
|
|
1
|
+
# candle-launch-bot
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Every Candle launch. First in line. Your size, your exit.**
|
|
4
|
+
|
|
5
|
+
Built by [Bruce Jeet](https://github.com/BruceJeet).
|
|
6
|
+
|
|
7
|
+
Somebody is first on every coin that drops on [Candle](https://candle.tv). Might as well be you.
|
|
8
|
+
|
|
9
|
+
candle-launch-bot watches the chain and Candle's token list at the same time, sizes in the second a launch clears your
|
|
10
|
+
filters, and works your exit while you sleep. One config file. One process. A local SQLite ledger that books what
|
|
11
|
+
actually landed, not what somebody quoted you.
|
|
12
|
+
|
|
13
|
+
- **Sees it first.** Two feeds race: Candle's public token list and the launch-curve program on chain (`logsSubscribe`,
|
|
14
|
+
signature polling as backup). First sighting wins, and both latencies get logged so you know which feed is carrying.
|
|
15
|
+
- **Picks its fights.** Launch mode, coin age, the price impact of your own buy on the curve, creator allow and deny
|
|
16
|
+
lists, name and symbol regex. Hard caps on ticket size, open bags, lifetime spend and entries per coin.
|
|
17
|
+
- **Pulls the trigger.** Candle's trade API, from your account's embedded wallet (Candle signs) or a TEE wallet you
|
|
18
|
+
sign for on your own box.
|
|
19
|
+
- **Gets paid.** `ladder`, `tpsl`, `time`, `preGrad`, `hold`, or drop in your own exit as one file.
|
|
20
|
+
- **Dry mode** runs the whole hunt with paper fills priced off Candle's own curve quote. Watch it work before it
|
|
21
|
+
spends a lamport.
|
|
22
|
+
|
|
23
|
+
Built on Candle's official SDK, [`@candledottv/agent-sdk`](https://www.npmjs.com/package/@candledottv/agent-sdk)
|
|
24
|
+
(pinned at 0.6.14). Solana only for now.
|
|
25
|
+
|
|
26
|
+
## House rules
|
|
27
|
+
|
|
28
|
+
The bot runs on four rules. They're in the code, not in a promise:
|
|
29
|
+
|
|
30
|
+
1. **Book what landed.** Every fill is read off the transaction. A quote is not a fill.
|
|
31
|
+
2. **Never sell what you can't see.** Every sell is clamped to the wallet's real balance, and the clamp only goes down.
|
|
32
|
+
3. **Never loop on your SOL.** Retries have a cap, a backoff and an end. A stuck order stops; it doesn't spin.
|
|
33
|
+
4. **One live transaction per order.** No double buys from a retry storm.
|
|
34
|
+
|
|
35
|
+
The full details are under "How it handles your money".
|
|
36
|
+
|
|
37
|
+
Words you'll see: a **launch mode** is how a Candle coin's bonding curve was set up (`open`, `exclusive`, and the
|
|
38
|
+
experimental `exp-a`, `exp-b`, `exp-c` and `exp-custom`, whose curves can take any shape); **graduation** is the moment
|
|
39
|
+
a curve fills and the coin moves to its DAMM v2 pool; a **TEE wallet** is a Candle trading wallet held in a secure
|
|
40
|
+
enclave (through Privy, Candle's wallet provider) and signed for by your key's signer.
|
|
41
|
+
|
|
42
|
+
## Get in (five minutes)
|
|
43
|
+
|
|
44
|
+
Node 22.18 or newer.
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
npx candle-launch-bot init # a few questions; writes bot.json in dry mode
|
|
48
|
+
npx candle-launch-bot scan --config bot.json --for 600 # print what it would buy; never trades
|
|
49
|
+
npx candle-launch-bot run --config bot.json # dry run: the whole loop with paper fills
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Dry run looks right? Set your API key, fund the wallet, and go live:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
export CANDLE_API_KEY=cndl_live_...
|
|
56
|
+
npx candle-launch-bot init --mode live --force [your flags again] # or edit bot.json: "mode": "live", your wallet
|
|
57
|
+
npx candle-launch-bot run --config bot.json
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`init` also runs without questions, from flags (`--yes`), for scripts and agents. `candle-launch-bot --help` lists the
|
|
61
|
+
flags:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
npx candle-launch-bot init --yes --payer embedded --sol 0.01 --max-open 3 --lifetime-sol 0.1 \
|
|
65
|
+
--modes open,exp-custom --max-age 60 --max-impact 300 --exit tpsl --take-profit 2 --stop-loss 0.5
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
With the API key in the environment, `init` looks up the key's wallet for you. Without a wallet, a dry config gets a
|
|
69
|
+
placeholder address; live mode needs the real one.
|
|
70
|
+
|
|
71
|
+
### Or let your agent set it up
|
|
72
|
+
|
|
73
|
+
Paste this to a coding agent (Claude Code, or any agent with a shell):
|
|
74
|
+
|
|
75
|
+
> Install and set up candle-launch-bot for me. Read `npx candle-launch-bot --help` and the package's README.md. Ask me
|
|
76
|
+
> for: payer (embedded or linked), ticket size in SOL, max open positions, lifetime SOL cap, launch modes, and the exit
|
|
77
|
+
> strategy. My Candle API key is in the env var CANDLE_API_KEY; never print it. Run `npx candle-launch-bot init --yes`
|
|
78
|
+
> with my answers, then `scan --for 600` and show me what it would have bought. Then start a dry run
|
|
79
|
+
> (`run --mode dry`) and show me `status.json` after a launch. Do not go live until I say so.
|
|
80
|
+
|
|
81
|
+
The package also ships an agent skill, `skills/candle-launch-bot/SKILL.md`, that walks an agent through the same steps.
|
|
82
|
+
|
|
83
|
+
## Setup in detail
|
|
84
|
+
|
|
85
|
+
### 1. Install
|
|
86
|
+
|
|
87
|
+
`npx candle-launch-bot ...` needs nothing else. To keep a copy: `npm install -g candle-launch-bot`. From source:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
git clone <this repo> && cd candle-launch-bot
|
|
91
|
+
npm ci
|
|
92
|
+
npm test
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
A source checkout runs the TypeScript in `src/` directly (Node strips the types); the package runs the compiled `dist/`.
|
|
96
|
+
|
|
97
|
+
### 2. Candle API key
|
|
98
|
+
|
|
99
|
+
Make an agent key for your Candle account (`cndl_live_…`; on candle.tv's agents page, or `candle keys create` with
|
|
100
|
+
the Candle CLI). Trading needs the **`swap:write`** scope, which a key only gets when it is asked for (pass it in the
|
|
101
|
+
key's scopes when you create it). The key decides which Candle profile the trades show under. Put the key in an
|
|
102
|
+
environment variable, never in the config file:
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
export CANDLE_API_KEY=cndl_live_...
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
If your account caps SOL per trade on the key, set the cap at or above `buy.sol`.
|
|
109
|
+
|
|
110
|
+
### 3. Pick a payer
|
|
111
|
+
|
|
112
|
+
| `payer` | Pays from | Signs | Needs |
|
|
113
|
+
|---|---|---|---|
|
|
114
|
+
| `embedded` | your Candle account's embedded Solana wallet | Candle, server-side, inside the trade call | the API key only |
|
|
115
|
+
| `linked` (default) | a TEE wallet bound to the API key | this machine, with the key's signer, through Candle's relay | the API key and the key signer (below) |
|
|
116
|
+
|
|
117
|
+
`embedded` is the simple one: it needs nothing but the API key, so it also suits a server with no OS keychain (a Linux
|
|
118
|
+
box without `secret-tool`, where the Candle CLI keeps its secrets in an encrypted file). At start the bot reads the key's embedded wallet (`GET /api/v1/agent/wallets/embedded`)
|
|
119
|
+
and refuses to run unless `wallet` is its Solana address, the key isn't denied the wallet, and the wallet is
|
|
120
|
+
delegated (so Candle can sign). There is no signer to set up: leave `signer`, `privyAppId`, `linkedWalletId` and
|
|
121
|
+
`privyWalletId` out (the config refuses them with `embedded`).
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{ "mode": "live", "payer": "embedded", "apiKeyEnv": "CANDLE_API_KEY", "wallet": "<your embedded wallet address>", ... }
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
With `embedded`, Candle signs and broadcasts inside the build call, so the bot can't save a signature before sending
|
|
128
|
+
(it can with `linked`). What it does instead:
|
|
129
|
+
|
|
130
|
+
- every order has one `clientTradeId` for life;
|
|
131
|
+
- a build that fails in any way other than Candle's own 4xx refusal counts as possibly executed (it counts against
|
|
132
|
+
`lifetimeSol` and `maxPerCoin`);
|
|
133
|
+
- the next attempt first reads Candle's trade job: `confirmed` is booked from its transaction, an id still in flight
|
|
134
|
+
is waited on (also at the attempt cap), and a same-id rebuild is Candle's replay of that trade, never a second one.
|
|
135
|
+
|
|
136
|
+
The embedded wallet is shared by the whole account: other API keys, the candle.tv app and you by hand all trade from
|
|
137
|
+
it. The bot's caps (`lifetimeSol`, `maxOpen`, `maxPerCoin`) count only this bot's own orders, not the wallet's other
|
|
138
|
+
trades. A sell of the same coin by someone else shows up as a wallet shortfall: the position is written down to what
|
|
139
|
+
the wallet still holds (alert `shortfall`). When a possibly executed buy can't be confirmed through Candle, the bot looks for its
|
|
140
|
+
transaction in the history of the wallet's token account for that coin (a buy since the order was made, for about the
|
|
141
|
+
ticket's SOL, landed while one of the order's attempts could have, not booked by another order, read back to the
|
|
142
|
+
order's creation) and books that (alert `found_tx`; two
|
|
143
|
+
such transactions are a double execution, booked together, alert `double_execution`). Without one, it adopts only tokens
|
|
144
|
+
that arrived *after* the order was made in buys that spent SOL (never a transfer or airdrop), and at most 1.5 x the
|
|
145
|
+
order's expected fill plus slippage (alert `adopted`).
|
|
146
|
+
Tokens that nothing explains yet may be an RPC that lags the balance: the order stays open and the scan repeats on the
|
|
147
|
+
backoff, up to 10 times (alert `rescan`). From then on the order only scans; it never builds again. A bag that was
|
|
148
|
+
already in the wallet, or anything bigger than this order could have bought, is left alone with an alert
|
|
149
|
+
(`unexplained_tokens`). `sell` on that coin reopens the ended buy's position by the same rule: its own transaction if the
|
|
150
|
+
history now shows it (its tokens, its cost), else only tokens bought since the order, capped (cost the ticket); never a
|
|
151
|
+
bag that was already there (alert `reopened`). Every 10 min the bot also compares the wallet's balance of each held coin
|
|
152
|
+
with what it booked (also for an hour after a position ends); more than booked on two readings a minute apart is
|
|
153
|
+
alerted (`surplus`), never adopted. Trading the bot's coins by hand in the same wallet confuses these checks; a wallet of its own (a separate account, or
|
|
154
|
+
`linked`) avoids all of it.
|
|
155
|
+
|
|
156
|
+
Everything else is the same for both payers: fills booked from the transaction, the sell clamp (on the wallet that
|
|
157
|
+
pays), capped retries, alerts.
|
|
158
|
+
|
|
159
|
+
### 4. TEE wallet and its signer (payer `linked`)
|
|
160
|
+
|
|
161
|
+
The bot pays from a Candle **TEE wallet bound to the API key**. At start it lists the wallets the key can trade
|
|
162
|
+
from (`GET /api/v1/agent/wallets/trading`) and refuses to run unless `wallet` is one of them. The same answer gives the
|
|
163
|
+
key's prefix and Candle's Privy app id, so `privyAppId`, `linkedWalletId` and `privyWalletId` need no config. Set them
|
|
164
|
+
only to pin values; a pin that disagrees with Candle stops the bot.
|
|
165
|
+
|
|
166
|
+
Signing happens on your machine with the key's P-256 **key signer**, made once with the Candle CLI on the machine that
|
|
167
|
+
will run the bot (`candle tee signer new --key <key prefix>`) and approved by the account owner
|
|
168
|
+
(`candle keys signer approve <code> --key <key prefix>`). Bind the wallet to the key with the CLI as Candle documents.
|
|
169
|
+
Then tell the bot where the signer is with `signer`:
|
|
170
|
+
|
|
171
|
+
| `signer.store` | Where the key is |
|
|
172
|
+
|---|---|
|
|
173
|
+
| `keychain` | Where the Candle CLI keeps it in the OS keychain (service `tv.candle.cli`): account `key_signer_<keyPrefix>_<spkiSha256>`, found through `keySigners.entries` in `~/.config/candle/config.json` by key prefix and the wallet's signer quorum, and checked against that index. For an imported (non-TEE) linked wallet, account `wallet_signer_<linkedWalletId>`. Uses `security` on macOS, `secret-tool` on Linux. A CLI that keeps its secrets in an encrypted file instead of an OS keychain can't be read this way: use `file` or `env`. |
|
|
174
|
+
| `file` | An `EncryptedFileSecretStore` file from the SDK (`path`, relative to the config file), passphrase in an env var (`passphraseEnv`), keyed by the wallet's linked-wallet id |
|
|
175
|
+
| `env` | The PEM (or the CLI's single-line base64 PKCS#8 form) in an env var (`pemEnv`) |
|
|
176
|
+
|
|
177
|
+
`keychain` reads where the Candle CLI stores the signer today (its config index and keychain account names). If a
|
|
178
|
+
later CLI version stores it differently, the bot reports the signer as missing at start; `file` or `env` don't depend
|
|
179
|
+
on the CLI.
|
|
180
|
+
|
|
181
|
+
### 5. Funding
|
|
182
|
+
|
|
183
|
+
Send SOL to the wallet that pays (embedded or TEE): tickets (`buy.sol` × the number of entries you allow) plus about 0.003 SOL per coin for
|
|
184
|
+
token-account rent and network fees. Every buy also pays Candle's platform fee on top of the ticket.
|
|
185
|
+
|
|
186
|
+
### 6. Run
|
|
187
|
+
|
|
188
|
+
```sh
|
|
189
|
+
candle-launch-bot init [--out bot.json] # write a config
|
|
190
|
+
candle-launch-bot scan --config bot.json # watch and print what it would buy; never trades
|
|
191
|
+
candle-launch-bot run --config bot.json --mode dry # the whole loop with paper fills
|
|
192
|
+
candle-launch-bot run --config bot.json # live, if the config says "mode": "live"
|
|
193
|
+
candle-launch-bot status --config bot.json # the last status.json
|
|
194
|
+
candle-launch-bot sell <mint> --config bot.json [--fraction 0.5]
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`sell` goes through the same order path as the bot (balance clamp, retries). If the bot is running, `sell` hands it
|
|
198
|
+
the request through `<stateDir>/requests/` and the bot runs it. A position whose exits Candle keeps refusing
|
|
199
|
+
(`exit_stuck`) gets one automatic try every `retry.stuckRetryMs` and an alert until it sells; `sell` tries at once.
|
|
200
|
+
|
|
201
|
+
Each cycle runs exits first (sell orders, then the strategy's new exits), then entries, within a time budget. A slow
|
|
202
|
+
market read while filtering times out after 3 s, and STOP or a signal is checked between steps and between orders.
|
|
203
|
+
|
|
204
|
+
## Controls (the brakes)
|
|
205
|
+
|
|
206
|
+
Files in the state dir, read at the top of every cycle:
|
|
207
|
+
|
|
208
|
+
- `STOP`: finish the cycle, write status, exit 0. The file stays: a restart exits again at once until you remove it.
|
|
209
|
+
- `PAUSE_BUYS`: no new entries; exits keep running.
|
|
210
|
+
|
|
211
|
+
`SIGINT`/`SIGTERM` stop the same way as `STOP`. A restart picks up from the ledger: open positions, orders in flight
|
|
212
|
+
(a signed transaction is checked on chain before anything is rebuilt), caps used.
|
|
213
|
+
|
|
214
|
+
## State dir
|
|
215
|
+
|
|
216
|
+
| File | What |
|
|
217
|
+
|---|---|
|
|
218
|
+
| `ledger.sqlite` | positions, orders, every attempt with its full error (program logs included), launches seen, alerts |
|
|
219
|
+
| `status.json` | written every cycle: equity, open positions with price and multiple, caps used, order-queue health, detection stats and latencies |
|
|
220
|
+
| `events.jsonl` | one JSON line per event (also on stdout) |
|
|
221
|
+
| `lock` | one process per state dir |
|
|
222
|
+
|
|
223
|
+
Every line that leaves the process is redacted first: the values of env vars whose names look secret
|
|
224
|
+
(`KEY`, `TOKEN`, `SECRET`, `PASSPHRASE`, `RPC`, …), the API key, the RPC URLs' keys, and common key patterns.
|
|
225
|
+
|
|
226
|
+
## Config reference
|
|
227
|
+
|
|
228
|
+
```json
|
|
229
|
+
{
|
|
230
|
+
"mode": "live",
|
|
231
|
+
"apiUrl": "https://api.alpha.candle.tv",
|
|
232
|
+
"apiKeyEnv": "CANDLE_API_KEY",
|
|
233
|
+
"rpcUrlEnv": "SOLANA_RPC_URL",
|
|
234
|
+
"rpcWsUrlEnv": "SOLANA_WS_URL",
|
|
235
|
+
"payer": "linked",
|
|
236
|
+
"wallet": "<TEE wallet address>",
|
|
237
|
+
"signer": { "store": "keychain" },
|
|
238
|
+
"stateDir": "./state",
|
|
239
|
+
"loopMs": 2000,
|
|
240
|
+
"buy": { "sol": 0.05, "maxSlippageBps": 500, "maxOpen": 3, "lifetimeSol": 1, "maxPriorityFeeLamports": 200000 },
|
|
241
|
+
"sell": { "maxSlippageBps": 1000 },
|
|
242
|
+
"filters": {
|
|
243
|
+
"chains": ["solana"],
|
|
244
|
+
"launchModes": ["open", "exp-custom"],
|
|
245
|
+
"maxAgeSec": 60,
|
|
246
|
+
"maxImpactBps": 300,
|
|
247
|
+
"creatorAllow": [],
|
|
248
|
+
"creatorDeny": [],
|
|
249
|
+
"nameMatch": "dog|cat",
|
|
250
|
+
"symbolMatch": "^[A-Z]{3,6}$",
|
|
251
|
+
"inferModeFromCurve": true
|
|
252
|
+
},
|
|
253
|
+
"detect": { "api": true, "chain": true, "apiPollMs": 3000, "chainPollMs": 15000, "apiLimit": 20 },
|
|
254
|
+
"exit": { "strategy": "ladder", "rungs": [{ "at": 2, "sell": 0.5 }, { "at": 5, "sell": 0.25 }], "stopLoss": 0.4 },
|
|
255
|
+
"retry": { "maxAttempts": 6, "baseDelayMs": 2000, "maxDelayMs": 120000, "maxExitOrders": 3, "stuckRetryMs": 600000, "builtProbeMs": 600000 },
|
|
256
|
+
"alerts": { "command": ["/usr/local/bin/notify-me", "launch-bot"], "webhookEnv": "ALERT_WEBHOOK_URL", "renotifyMs": 3600000 }
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Unknown keys are refused at every level, so a typo fails at start instead of being ignored.
|
|
261
|
+
|
|
262
|
+
| Key | Meaning |
|
|
263
|
+
|---|---|
|
|
264
|
+
| `mode` | `dry` (decide and log, paper fills) or `live` |
|
|
265
|
+
| `apiUrl` | Candle API host (must be https) |
|
|
266
|
+
| `apiKeyEnv` | name of the env var holding the API key |
|
|
267
|
+
| `rpcUrlEnv`, `rpcWsUrlEnv` | env vars with your Solana RPC http and websocket URLs. Default: the public mainnet RPC, websocket derived from the http URL |
|
|
268
|
+
| `payer` | `embedded` (Candle signs; API key only) or `linked` (TEE wallet, local key signer; default) |
|
|
269
|
+
| `wallet` | the wallet that pays: the key's embedded wallet, or the TEE wallet |
|
|
270
|
+
| `linkedWalletId`, `privyWalletId`, `privyAppId` | optional pins; Candle supplies all three, and a pin that disagrees stops the bot |
|
|
271
|
+
| `buy.sol` | SOL per ticket |
|
|
272
|
+
| `buy.maxSlippageBps` | slippage tolerance on buys |
|
|
273
|
+
| `buy.maxOpen` | most positions open at once (open, opening, or stuck) |
|
|
274
|
+
| `buy.lifetimeSol` | most SOL in tickets ever committed to buys by this state dir. Candle's platform fee (its `bps`, about 0.25 %) and network fees come on top. A buy refused before anything was signed, or reverted, doesn't count; a refusal after signing does, because it might have landed |
|
|
275
|
+
| `buy.maxPerCoin` | entries per coin: 1 (the only value; re-entering a coin is not supported) |
|
|
276
|
+
| `buy.maxPriorityFeeLamports`, `sell.maxPriorityFeeLamports` | priority-fee ceiling Candle's build may bid (Jupiter venue) |
|
|
277
|
+
| `sell.maxSlippageBps` | slippage tolerance on sells (default: the larger of the buy's and 1000) |
|
|
278
|
+
| `filters.launchModes` | Candle launch modes to buy: `open`, `exclusive`, `exp-a`, `exp-b`, `exp-c`, `exp-custom` (also `test-open`, `test-exclusive`); anything else is a config error |
|
|
279
|
+
| `filters.maxAgeSec` | skip a coin older than this at decision time (age from the launch transaction's block time) |
|
|
280
|
+
| `filters.maxImpactBps` | skip when your ticket's average fill price would be more than this above the spot price (`s1/s0 - 1` on the curve's square-root price, after the curve's fee), from the curve account; checked again before every buy retry |
|
|
281
|
+
| `filters.inferModeFromCurve` | a launch seen on chain before Candle's list has it counts as `open` when its curve account matches the open preset exactly (threshold, fee, both liquidity segments, start price). Every other curve, `exclusive` and all `exp-*` modes included, waits for Candle's `tier`, since an exp-custom curve can take any threshold. Default true |
|
|
282
|
+
| `filters.creatorAllow` / `creatorDeny` | addresses; matched against the token list's creator and the launch transaction's signer. The bot's own `wallet` is always denied |
|
|
283
|
+
| `filters.nameMatch` / `symbolMatch` | case-insensitive regular expressions |
|
|
284
|
+
| `detect.*` | turn a source off, set poll intervals |
|
|
285
|
+
| `exit` | `{ "strategy": "<name>", ...params }`, or `{ "strategy": "x", "module": "./my-exit.ts", ... }` |
|
|
286
|
+
| `retry.maxAttempts` | attempts per order before it is terminal |
|
|
287
|
+
| `retry.baseDelayMs`, `maxDelayMs` | exponential backoff with jitter, per order |
|
|
288
|
+
| `retry.maxExitOrders` | exit orders Candle *refuses* (or that revert) before a position goes `exit_stuck`. Orders that run out of attempts on network errors, timeouts, 5xx or rate limits don't count: the next exit is minted on the position's backoff, without limit |
|
|
289
|
+
| `retry.builtProbeMs` | payer `embedded`: a trade Candle still shows as `built` after this long (default 10 min) gets one same-id build that asks Candle for its signature. For a buy it runs only while the buy would still be placed (before its deadline, buys not paused, impact passing) |
|
|
290
|
+
| `retry.builtHoldMs` | payer `embedded`: a buy past its deadline whose job stays `built` this long (default 6 h) is ended after a wallet scan (alert `built_held`); it stays counted in `lifetimeSol` |
|
|
291
|
+
| `retry.stuckRetryMs` | an `exit_stuck` position still gets one more exit try this often (default 10 min), and its alert keeps repeating until an exit fills |
|
|
292
|
+
| `alerts.command` | argv run (no shell) for each alert; the alert is JSON on stdin and in `ALERT_KEY` / `ALERT_MESSAGE`. It gets a minimal environment: `PATH`, those two, and the names in `alerts.commandEnv`; never the API key, signer or RPC URL |
|
|
293
|
+
| `alerts.commandEnv` | extra env var names to pass to the alert command |
|
|
294
|
+
| `alerts.webhookEnv` | env var holding a URL that gets each alert as a JSON POST |
|
|
295
|
+
| `alerts.renotifyMs` | how often a condition that lasts (an `exit_stuck` position, a fill that can't be read) is announced again; default 1 h |
|
|
296
|
+
| `alerts.holdAlertMs` | exits held for want of a sell route (migrating, pool at its floor, no price, unreadable) longer than this reach the alert sinks as `no_route`; default 5 min |
|
|
297
|
+
|
|
298
|
+
A filter that needs a field the bot doesn't have yet (a launch seen on chain before Candle's list has its mode or
|
|
299
|
+
name) waits for it and asks Candle's market endpoint. A field that never arrives never counts as a pass.
|
|
300
|
+
|
|
301
|
+
## Exit strategies
|
|
302
|
+
|
|
303
|
+
| Strategy | Params | Does |
|
|
304
|
+
|---|---|---|
|
|
305
|
+
| `hold` | none | never sells by itself |
|
|
306
|
+
| `tpsl` | `takeProfit` (×), `stopLoss` (×), `trailing` (fraction off the peak) | sells all on any of them |
|
|
307
|
+
| `time` | `afterSec` | sells all N seconds after entry |
|
|
308
|
+
| `ladder` | `rungs: [{ at, sell }]`, `stopLoss` | sells `sell` of the original size at each multiple, keeps the rest as a core |
|
|
309
|
+
| `preGrad` | `progress` (0..1), `fraction`, `stopLoss` | sells before graduation at a curve-progress threshold |
|
|
310
|
+
|
|
311
|
+
Multiples are against your all-in entry price (SOL spent including Candle's fee, over tokens received). `preGrad`
|
|
312
|
+
exists because graduated Candle coins sell only through Jupiter, and `exp-custom` pools often sit at their price floor
|
|
313
|
+
with no sell route at all. Selling while the curve can still take it avoids that.
|
|
314
|
+
|
|
315
|
+
### Writing one
|
|
316
|
+
|
|
317
|
+
A strategy is one file that exports `create(params)` and returns `{ name, onTick }`. Put it next to your config and
|
|
318
|
+
point `exit.module` at it (a path relative to the config file); the other keys of `exit` are its params. The engine
|
|
319
|
+
needs no change. A `.js` file works everywhere; a `.ts` file works too on Node 22.18+ (outside `node_modules`).
|
|
320
|
+
|
|
321
|
+
```js
|
|
322
|
+
// my-exit.js "exit": { "strategy": "breakeven", "module": "./my-exit.js", "arm": 1.5 }
|
|
323
|
+
export function create(params) {
|
|
324
|
+
const arm = Number(params.arm ?? 1.5);
|
|
325
|
+
return {
|
|
326
|
+
name: "breakeven",
|
|
327
|
+
onTick(p, m) {
|
|
328
|
+
if (m.priceSol === null) return { action: "hold" };
|
|
329
|
+
const armed = p.peakPriceSol >= p.entryPriceSol * arm;
|
|
330
|
+
return armed && m.priceSol <= p.entryPriceSol ? { action: "sell", fraction: 1, reason: "back to entry" } : { action: "hold" };
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
`examples/strategies/` has three to start from: `timeboxed-tp.js` (take profit inside a time window, else sell when it
|
|
337
|
+
closes), `breakeven-stop.js` (a stop that arms at a multiple and sits at entry) and `stale-coin.js` (sell a coin that
|
|
338
|
+
has not moved after a while). Types for TypeScript authors: `import type { ExitStrategy } from "candle-launch-bot"`.
|
|
339
|
+
|
|
340
|
+
`onTick(position, market)` runs once per cycle for each open position with no exit order in flight:
|
|
341
|
+
|
|
342
|
+
- `position`: `mint, openedAtMs, qtyRaw, remainingRaw, decimals, entryPriceSol, peakPriceSol, soldFraction`
|
|
343
|
+
- `market`: `priceSol` (null if unreadable this cycle), `progress` (graduation, 0..1), `migrated`, `venue`
|
|
344
|
+
(`curve` / `damm`), `nowMs`
|
|
345
|
+
- return `{ action: "hold" }` or `{ action: "sell", fraction, reason }`, where `fraction` is a share of what remains
|
|
346
|
+
|
|
347
|
+
## How it handles your money
|
|
348
|
+
|
|
349
|
+
- **Quantity is what the chain says.** After a fill the bot reads the transaction and books the wallet's token and
|
|
350
|
+
SOL balance changes, with the network fee and token-account rent kept apart. If the transaction can't be read it
|
|
351
|
+
keeps reading; after several failed reads it asks Candle again (`confirm` replay) for `actualOutRaw`, else books the
|
|
352
|
+
build's `minOutRaw`, and raises
|
|
353
|
+
an alert. Every position records which source it used.
|
|
354
|
+
- **Sells are clamped** to the wallet's on-chain balance when the order is made, less what the bot's other positions
|
|
355
|
+
in the same coin own. The clamp only reduces, never sells more than the position holds (so tokens held by others in
|
|
356
|
+
the same wallet are left alone), and a zero or unreadable balance refuses the sell and raises an alert. When the
|
|
357
|
+
wallet holds less than the ledger, the position is written down to the balance (alert `shortfall`) so it can close.
|
|
358
|
+
- **One live transaction per order.** The signature and the transaction's blockhash are saved before submit. Before
|
|
359
|
+
any rebuild, the saved signature is checked on chain, and the rebuild waits until the chain says that blockhash can no
|
|
360
|
+
longer be used (`isBlockhashValid`, judged by block height, not by the clock). Rebuilds reuse the order's
|
|
361
|
+
`clientTradeId`. `IDEMPOTENCY_CONFLICT` is never treated as a failure: `prior_attempt_on_chain` is booked as the fill,
|
|
362
|
+
and anything else waits and reconciles.
|
|
363
|
+
- **Retries** have a cap, jittered exponential backoff on each order's own clock, and a terminal state. A throw costs
|
|
364
|
+
an attempt like any failure. Errors are read by `code`, `status`, `stage` and `reason`, never by message text.
|
|
365
|
+
Only Candle's own non-retryable 4xx refusal at build or sign (nothing broadcast yet) ends an order at once. Network
|
|
366
|
+
errors (`fetch failed` with ECONNRESET, ENOTFOUND, …), timeouts and 5xx are always retried. A refusal from Candle's
|
|
367
|
+
submit is treated as possibly broadcast, because it doesn't say whether the transaction went out: the bot waits and
|
|
368
|
+
reconciles on chain before it builds again. Every SDK and RPC call has a timeout, and settling a fill never sleeps
|
|
369
|
+
inside the loop.
|
|
370
|
+
- **Exits wait for a route.** While a coin is migrating to its pool (its curve full, or flagged migrated with the pool
|
|
371
|
+
not there yet; about 3.5 min), has no readable price, or its pool sits at its price floor, no sell is placed and no
|
|
372
|
+
exit attempt is used up.
|
|
373
|
+
- **A conflict is never a failure.** An order Candle answered with `IDEMPOTENCY_CONFLICT` is not rebuilt and can't go
|
|
374
|
+
terminal until Candle's trade job settles: `confirmed` is booked, gone or failed allows a same-id rebuild.
|
|
375
|
+
- **Stale buys stop.** A buy order carries a deadline (launch time + `maxAgeSec`) and is never built past it, nor
|
|
376
|
+
while `PAUSE_BUYS` is set. A dry run never touches live orders, and a state dir holds one mode only.
|
|
377
|
+
- **Alerts** carry the worsening dimension in their key (attempt buckets, terminal states, stuck exits), so a
|
|
378
|
+
condition that gets worse raises a new alert; a repeat of the same one is announced again every `alerts.renotifyMs`.
|
|
379
|
+
|
|
380
|
+
## Know the terrain
|
|
381
|
+
|
|
382
|
+
- **Thin curves.** Candle curves start thin: a ticket of a few tenths of a SOL can move the price several percent.
|
|
383
|
+
`filters.maxImpactBps` is checked against the curve before every buy.
|
|
384
|
+
- **Graduated coins.** After graduation a coin trades in its DAMM v2 pool and sells route through Jupiter. Some
|
|
385
|
+
`exp-custom` pools sit at their price floor with no sell route; the bot holds exits until a route exists and alerts
|
|
386
|
+
when that lasts (`alerts.holdAlertMs`). `preGrad` sells while the curve can still take it.
|
|
387
|
+
- **Shared embedded wallet.** With `payer: "embedded"` the wallet belongs to the whole account (see "Pick a payer").
|
|
388
|
+
- **One entry per coin.** `buy.maxPerCoin` is 1; re-entering a coin is not supported.
|
|
389
|
+
- **The keychain signer follows the Candle CLI's storage layout** (see "TEE wallet and its signer").
|
|
390
|
+
- **Solana only.**
|
|
391
|
+
|
|
392
|
+
## Development
|
|
393
|
+
|
|
394
|
+
`npm test` runs the suite (`node --test`, no network). `npm run typecheck` checks types and `npm run build` compiles
|
|
395
|
+
`dist/` (run on `npm pack`). See CONTRIBUTING.md.
|
|
396
|
+
|
|
397
|
+
## License
|
|
398
|
+
|
|
399
|
+
MIT. Fork it, tune it, run it.
|
|
400
|
+
|
|
401
|
+
Ladder out. Keep a core. Keep the receipts.
|
|
@@ -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/alerts.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Ledger } from "./ledger.ts";
|
|
2
|
+
import type { Logger } from "./log.ts";
|
|
3
|
+
export interface AlertConfig {
|
|
4
|
+
command?: string[];
|
|
5
|
+
/** Env var names passed to the command besides PATH, ALERT_KEY, ALERT_MESSAGE (nothing else is inherited). */
|
|
6
|
+
commandEnv?: string[];
|
|
7
|
+
webhookEnv?: string;
|
|
8
|
+
renotifyMs: number;
|
|
9
|
+
}
|
|
10
|
+
export type AlertSink = (a: {
|
|
11
|
+
key: string;
|
|
12
|
+
message: string;
|
|
13
|
+
at: string;
|
|
14
|
+
}) => Promise<void>;
|
|
15
|
+
export declare function commandSink(argv: string[], timeoutMs?: number, passEnv?: string[], env?: NodeJS.ProcessEnv): AlertSink;
|
|
16
|
+
export declare function webhookSink(url: string, f?: typeof fetch, timeoutMs?: number): AlertSink;
|
|
17
|
+
export declare class Alerter {
|
|
18
|
+
private ledger;
|
|
19
|
+
private log;
|
|
20
|
+
private sinks;
|
|
21
|
+
private now;
|
|
22
|
+
readonly renotifyMs: number;
|
|
23
|
+
sent: number;
|
|
24
|
+
constructor(ledger: Ledger, log: Logger, sinks: AlertSink[], renotifyMs: number, now?: () => number);
|
|
25
|
+
/** Record an alert; announce it (log + sinks) when it is new or its renotify time has come. */
|
|
26
|
+
raise(key: string, message: string, fields?: Record<string, unknown>): boolean;
|
|
27
|
+
}
|
|
28
|
+
export declare function makeAlerter(cfg: AlertConfig, ledger: Ledger, log: Logger, env?: NodeJS.ProcessEnv, now?: () => number): Alerter;
|
package/dist/alerts.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alerts that leave the process. Every alert is deduped in the ledger under a key that carries the worsening
|
|
3
|
+
* dimension (attempt bucket, terminal state); a key is announced when first seen and again after `renotifyMs`
|
|
4
|
+
* while it keeps recurring. Conditions that need a human (exit_stuck, a fill that cannot be read) are re-raised by
|
|
5
|
+
* the engine every cycle, so they re-announce on that schedule until cleared.
|
|
6
|
+
*
|
|
7
|
+
* Sinks (both optional, both redacted, both bounded by a timeout, neither can stop the bot):
|
|
8
|
+
* - command: an argv list run without a shell; the alert is JSON on its stdin, ALERT_KEY / ALERT_MESSAGE in env
|
|
9
|
+
* - webhook: POST JSON {key, message, at} to the URL held in the env var named by webhookEnv
|
|
10
|
+
*/
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
import { addSecret, redact } from "./redact.js";
|
|
13
|
+
export function commandSink(argv, timeoutMs = 10_000, passEnv = [], env = process.env) {
|
|
14
|
+
return (a) => new Promise((resolve) => {
|
|
15
|
+
try {
|
|
16
|
+
// A minimal environment: the API key, signer, RPC URL and webhook never reach the alert command.
|
|
17
|
+
const childEnv = { PATH: env.PATH ?? "/usr/bin:/bin", ALERT_KEY: a.key, ALERT_MESSAGE: a.message };
|
|
18
|
+
for (const n of passEnv)
|
|
19
|
+
if (env[n] !== undefined)
|
|
20
|
+
childEnv[n] = env[n];
|
|
21
|
+
const child = spawn(argv[0], argv.slice(1), { stdio: ["pipe", "ignore", "ignore"], env: childEnv, timeout: timeoutMs });
|
|
22
|
+
child.on("error", () => resolve());
|
|
23
|
+
child.on("exit", () => resolve());
|
|
24
|
+
child.stdin.on("error", () => undefined);
|
|
25
|
+
child.stdin.end(JSON.stringify(a) + "\n");
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
resolve();
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export function webhookSink(url, f = fetch, timeoutMs = 10_000) {
|
|
33
|
+
addSecret(url);
|
|
34
|
+
return async (a) => {
|
|
35
|
+
try {
|
|
36
|
+
await f(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(a), signal: AbortSignal.timeout(timeoutMs) });
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
/* a dead webhook never stops the bot; the ledger and the log keep the alert */
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export class Alerter {
|
|
44
|
+
ledger;
|
|
45
|
+
log;
|
|
46
|
+
sinks;
|
|
47
|
+
now;
|
|
48
|
+
renotifyMs;
|
|
49
|
+
sent = 0;
|
|
50
|
+
constructor(ledger, log, sinks, renotifyMs, now = Date.now) {
|
|
51
|
+
this.ledger = ledger;
|
|
52
|
+
this.log = log;
|
|
53
|
+
this.sinks = sinks;
|
|
54
|
+
this.renotifyMs = renotifyMs;
|
|
55
|
+
this.now = now;
|
|
56
|
+
}
|
|
57
|
+
/** Record an alert; announce it (log + sinks) when it is new or its renotify time has come. */
|
|
58
|
+
raise(key, message, fields = {}) {
|
|
59
|
+
const due = this.ledger.alert(key, message, this.now(), this.renotifyMs);
|
|
60
|
+
if (!due)
|
|
61
|
+
return false;
|
|
62
|
+
const msg = redact(message);
|
|
63
|
+
this.log.warn("alert", { key, message: msg, ...fields });
|
|
64
|
+
const a = { key: redact(key), message: msg, at: new Date(this.now()).toISOString() };
|
|
65
|
+
this.sent++;
|
|
66
|
+
for (const s of this.sinks)
|
|
67
|
+
void s(a).catch(() => undefined);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function makeAlerter(cfg, ledger, log, env = process.env, now) {
|
|
72
|
+
const sinks = [];
|
|
73
|
+
if (cfg.command?.length)
|
|
74
|
+
sinks.push(commandSink(cfg.command, 10_000, cfg.commandEnv ?? [], env));
|
|
75
|
+
if (cfg.webhookEnv) {
|
|
76
|
+
const url = env[cfg.webhookEnv];
|
|
77
|
+
if (!url)
|
|
78
|
+
throw new Error(`alerts.webhookEnv: ${cfg.webhookEnv} is not set`);
|
|
79
|
+
sinks.push(webhookSink(url));
|
|
80
|
+
}
|
|
81
|
+
return new Alerter(ledger, log, sinks, cfg.renotifyMs, now);
|
|
82
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const HELP = "candle-launch-bot: buys Candle launches as they appear and exits them by a strategy.\n\n candle-launch-bot init [--out bot.json] [--yes] [flags] write a config (dry mode unless --mode live)\n candle-launch-bot scan --config bot.json [--for 600] detect and print what it would buy; never trades\n candle-launch-bot run --config bot.json [--mode dry|live] run the bot\n candle-launch-bot status --config bot.json print status.json\n candle-launch-bot sell <mint> --config bot.json [--fraction 1]\n\ninit flags (all optional; with --yes nothing is asked; defaults shown):\n --mode dry|live (dry) --payer embedded|linked (embedded) --key-env <env var with the API key> (CANDLE_API_KEY)\n --wallet <address> (looked up when the key is set) --api-url <https url>\n --sol 0.01 --max-open 3 --lifetime-sol (10 tickets) --modes open,exp-custom --max-age 60 --max-impact 300\n --slippage 500\n --exit hold|tpsl|time|ladder|preGrad (tpsl) --take-profit 2 --stop-loss 0.5 --trailing <0-1> --after-sec 900\n --progress 0.8 --fraction 1 --rungs 2:0.3,5:0.3,10:0.2\n --exit-module ./my-exit.js --exit-params '{\"arm\":1.5}' your own strategy file and its params\n --signer keychain|file|env (keychain) --signer-env <env var with the PEM> --signer-path <file>\n --signer-pass-env <env var with the file's passphrase> payer linked only\n --rpc-env <env var with an RPC url> --rpc-ws-env <env var with a websocket url>\n --webhook <env var with an alert webhook url> --state-dir <dir> (./state-dry or ./state-live)\n --out bot.json --force (overwrite)\n\nControls: create STOP or PAUSE_BUYS in the state folder. Docs: 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>;
|