solana-traderclaw 1.0.36 → 1.0.38
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/bin/installer-step-engine.mjs +1 -1
- package/bin/openclaw-trader.mjs +243 -1
- package/bin/traderclaw.cjs +0 -0
- package/dist/{chunk-QSICXLW7.js → chunk-GYXPEC7O.js} +47 -30
- package/dist/index.js +99 -8
- package/dist/src/session-manager.js +1 -1
- package/package.json +1 -1
- package/skills/solana-trader/SKILL.md +25 -18
|
@@ -619,7 +619,7 @@ function traderCronPrescriptiveJobs(agentId) {
|
|
|
619
619
|
schedule: "0 * * * *",
|
|
620
620
|
agentId,
|
|
621
621
|
message:
|
|
622
|
-
"CRON_JOB: alpha_scan — Call solana_scan_launches to find new token launches from the last hour. For each candidate, call solana_token_snapshot to check price, volume, and age. Filter for: 24h volume above 50000 USD, market cap above 10000 USD. For tokens that pass, call solana_token_holders to check holder distribution (skip if top holder owns more than 30 percent), then solana_token_risk to check for mint/freeze authority (hard skip if present). If a token passes all checks,
|
|
622
|
+
"CRON_JOB: alpha_scan — Call solana_scan_launches to find new token launches from the last hour. For each candidate, call solana_token_snapshot to check price, volume, and age. Filter for: 24h volume above 50000 USD, market cap above 10000 USD. For tokens that pass, call solana_token_holders to check holder distribution (skip if top holder owns more than 30 percent), then solana_token_risk to check for mint/freeze authority (hard skip if present). If a token passes all checks, use solana_trade_precheck and consider trade execution using solana_trade_execute. Log all scanned tokens and results using solana_memory_write. Do not ask questions.",
|
|
623
623
|
enabled: true,
|
|
624
624
|
},
|
|
625
625
|
{
|
package/bin/openclaw-trader.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { createInterface } from "readline";
|
|
4
|
-
import { readFileSync, writeFileSync, mkdirSync, appendFileSync } from "fs";
|
|
4
|
+
import { readFileSync, writeFileSync, mkdirSync, appendFileSync, existsSync } from "fs";
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
import { homedir } from "os";
|
|
7
7
|
import { randomUUID, createPrivateKey, sign as cryptoSign } from "crypto";
|
|
@@ -2596,6 +2596,242 @@ async function cmdInstall(args) {
|
|
|
2596
2596
|
printInfo("Press Ctrl+C to stop the wizard server.");
|
|
2597
2597
|
}
|
|
2598
2598
|
|
|
2599
|
+
async function cmdTestSession(args) {
|
|
2600
|
+
const config = readConfig();
|
|
2601
|
+
const pluginConfig = getPluginConfig(config);
|
|
2602
|
+
|
|
2603
|
+
if (!pluginConfig) {
|
|
2604
|
+
printError("No plugin configuration found. Run 'traderclaw setup' first.");
|
|
2605
|
+
process.exit(1);
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
const orchestratorUrl = pluginConfig.orchestratorUrl;
|
|
2609
|
+
if (!orchestratorUrl) {
|
|
2610
|
+
printError("orchestratorUrl not set in config. Run 'traderclaw setup' to fix.");
|
|
2611
|
+
process.exit(1);
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
const dataDir = pluginConfig.dataDir || join(process.cwd(), ".traderclaw-v1-data");
|
|
2615
|
+
const sessionTokensPath = join(dataDir, "session-tokens.json");
|
|
2616
|
+
|
|
2617
|
+
let sidecar = null;
|
|
2618
|
+
try {
|
|
2619
|
+
if (existsSync(sessionTokensPath)) {
|
|
2620
|
+
sidecar = JSON.parse(readFileSync(sessionTokensPath, "utf-8"));
|
|
2621
|
+
}
|
|
2622
|
+
} catch { /* ignore */ }
|
|
2623
|
+
|
|
2624
|
+
const effectiveRefreshToken =
|
|
2625
|
+
(sidecar?.refreshToken && sidecar.refreshToken.length > 0)
|
|
2626
|
+
? sidecar.refreshToken
|
|
2627
|
+
: pluginConfig.refreshToken;
|
|
2628
|
+
|
|
2629
|
+
const walletPrivateKeyInput = args.includes("--wallet-private-key")
|
|
2630
|
+
? args[args.indexOf("--wallet-private-key") + 1] || ""
|
|
2631
|
+
: "";
|
|
2632
|
+
|
|
2633
|
+
print("\nTraderClaw V1 — Session Auth Test\n");
|
|
2634
|
+
print("=".repeat(50));
|
|
2635
|
+
printInfo(` Orchestrator: ${orchestratorUrl}`);
|
|
2636
|
+
printInfo(` API key: ${pluginConfig.apiKey ? maskKey(pluginConfig.apiKey) : "MISSING"}`);
|
|
2637
|
+
printInfo(` Refresh token: ${effectiveRefreshToken ? maskKey(effectiveRefreshToken) : "MISSING"}`);
|
|
2638
|
+
printInfo(` Sidecar file: ${sidecar ? sessionTokensPath : "not found"}`);
|
|
2639
|
+
printInfo(` Wallet pub key: ${pluginConfig.walletPublicKey || "not set"}`);
|
|
2640
|
+
printInfo(` Wallet priv key: ${getRuntimeWalletPrivateKey(walletPrivateKeyInput) ? "available" : "NOT AVAILABLE"}`);
|
|
2641
|
+
print("");
|
|
2642
|
+
|
|
2643
|
+
const results = [];
|
|
2644
|
+
let currentAccessToken = null;
|
|
2645
|
+
let currentRefreshToken = effectiveRefreshToken;
|
|
2646
|
+
|
|
2647
|
+
// --- Test 1: Initial refresh ---
|
|
2648
|
+
print(" [1/5] Token refresh...");
|
|
2649
|
+
const t1Start = Date.now();
|
|
2650
|
+
try {
|
|
2651
|
+
if (!currentRefreshToken) {
|
|
2652
|
+
throw new Error("No refresh token available — skip to challenge flow test");
|
|
2653
|
+
}
|
|
2654
|
+
const tokens = await doRefresh(orchestratorUrl, currentRefreshToken);
|
|
2655
|
+
if (!tokens) {
|
|
2656
|
+
printWarn(" Refresh returned null (token revoked/expired) — will test challenge flow");
|
|
2657
|
+
results.push({ test: "initial_refresh", status: "expired", ms: Date.now() - t1Start });
|
|
2658
|
+
currentRefreshToken = null;
|
|
2659
|
+
} else {
|
|
2660
|
+
const ms = Date.now() - t1Start;
|
|
2661
|
+
currentAccessToken = tokens.accessToken;
|
|
2662
|
+
currentRefreshToken = tokens.refreshToken;
|
|
2663
|
+
printSuccess(` OK (${ms}ms) — accessTokenTtl: ${tokens.accessTokenTtlSeconds}s, refreshTokenTtl: ${tokens.refreshTokenTtlSeconds}s`);
|
|
2664
|
+
results.push({
|
|
2665
|
+
test: "initial_refresh",
|
|
2666
|
+
status: "ok",
|
|
2667
|
+
ms,
|
|
2668
|
+
accessTokenTtl: tokens.accessTokenTtlSeconds,
|
|
2669
|
+
refreshTokenTtl: tokens.refreshTokenTtlSeconds,
|
|
2670
|
+
tier: tokens.session?.tier,
|
|
2671
|
+
});
|
|
2672
|
+
}
|
|
2673
|
+
} catch (err) {
|
|
2674
|
+
printError(` FAIL: ${err.message}`);
|
|
2675
|
+
results.push({ test: "initial_refresh", status: "fail", ms: Date.now() - t1Start, error: err.message });
|
|
2676
|
+
currentRefreshToken = null;
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
// --- Test 2: Second refresh (verifies rotation worked) ---
|
|
2680
|
+
print(" [2/5] Second refresh (token rotation check)...");
|
|
2681
|
+
const t2Start = Date.now();
|
|
2682
|
+
try {
|
|
2683
|
+
if (!currentRefreshToken) {
|
|
2684
|
+
throw new Error("No refresh token — skipped (previous test failed)");
|
|
2685
|
+
}
|
|
2686
|
+
const tokens2 = await doRefresh(orchestratorUrl, currentRefreshToken);
|
|
2687
|
+
if (!tokens2) {
|
|
2688
|
+
printError(" FAIL: second refresh returned null — rotation may be broken");
|
|
2689
|
+
results.push({ test: "rotation_check", status: "fail", ms: Date.now() - t2Start, error: "null response" });
|
|
2690
|
+
} else {
|
|
2691
|
+
const ms = Date.now() - t2Start;
|
|
2692
|
+
const rotated = tokens2.refreshToken !== currentRefreshToken;
|
|
2693
|
+
currentAccessToken = tokens2.accessToken;
|
|
2694
|
+
currentRefreshToken = tokens2.refreshToken;
|
|
2695
|
+
if (rotated) {
|
|
2696
|
+
printSuccess(` OK (${ms}ms) — refresh token rotated correctly`);
|
|
2697
|
+
} else {
|
|
2698
|
+
printWarn(` OK (${ms}ms) — refresh token NOT rotated (server may use static tokens)`);
|
|
2699
|
+
}
|
|
2700
|
+
results.push({ test: "rotation_check", status: "ok", ms, rotated });
|
|
2701
|
+
}
|
|
2702
|
+
} catch (err) {
|
|
2703
|
+
printError(` FAIL: ${err.message}`);
|
|
2704
|
+
results.push({ test: "rotation_check", status: "fail", ms: Date.now() - t2Start, error: err.message });
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
// --- Test 3: API call with access token ---
|
|
2708
|
+
print(" [3/5] Authenticated API call (/healthz)...");
|
|
2709
|
+
const t3Start = Date.now();
|
|
2710
|
+
try {
|
|
2711
|
+
if (!currentAccessToken) {
|
|
2712
|
+
throw new Error("No access token — skipped");
|
|
2713
|
+
}
|
|
2714
|
+
const health = await httpRequest(`${orchestratorUrl}/healthz`, {
|
|
2715
|
+
accessToken: currentAccessToken,
|
|
2716
|
+
timeout: 8000,
|
|
2717
|
+
});
|
|
2718
|
+
const ms = Date.now() - t3Start;
|
|
2719
|
+
if (health.ok) {
|
|
2720
|
+
printSuccess(` OK (${ms}ms) — orchestrator healthy`);
|
|
2721
|
+
results.push({ test: "api_call", status: "ok", ms });
|
|
2722
|
+
} else {
|
|
2723
|
+
printError(` FAIL: HTTP ${health.status}`);
|
|
2724
|
+
results.push({ test: "api_call", status: "fail", ms, error: `HTTP ${health.status}` });
|
|
2725
|
+
}
|
|
2726
|
+
} catch (err) {
|
|
2727
|
+
printError(` FAIL: ${err.message}`);
|
|
2728
|
+
results.push({ test: "api_call", status: "fail", ms: Date.now() - t3Start, error: err.message });
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
// --- Test 4: Challenge flow (re-auth from scratch) ---
|
|
2732
|
+
print(" [4/5] Challenge flow (full re-authentication)...");
|
|
2733
|
+
const t4Start = Date.now();
|
|
2734
|
+
try {
|
|
2735
|
+
if (!pluginConfig.apiKey) {
|
|
2736
|
+
throw new Error("No API key — cannot test challenge flow");
|
|
2737
|
+
}
|
|
2738
|
+
const challenge = await doChallenge(orchestratorUrl, pluginConfig.apiKey, pluginConfig.walletPublicKey);
|
|
2739
|
+
const ms = Date.now() - t4Start;
|
|
2740
|
+
if (challenge.walletProofRequired) {
|
|
2741
|
+
const wpk = getRuntimeWalletPrivateKey(walletPrivateKeyInput);
|
|
2742
|
+
if (wpk) {
|
|
2743
|
+
try {
|
|
2744
|
+
const walletSig = signChallengeLocally(challenge.challenge, wpk);
|
|
2745
|
+
const tokens = await doSessionStart(
|
|
2746
|
+
orchestratorUrl,
|
|
2747
|
+
pluginConfig.apiKey,
|
|
2748
|
+
challenge.challengeId,
|
|
2749
|
+
challenge.walletPublicKey || pluginConfig.walletPublicKey,
|
|
2750
|
+
walletSig,
|
|
2751
|
+
);
|
|
2752
|
+
const totalMs = Date.now() - t4Start;
|
|
2753
|
+
currentAccessToken = tokens.accessToken;
|
|
2754
|
+
currentRefreshToken = tokens.refreshToken;
|
|
2755
|
+
printSuccess(` OK (${totalMs}ms) — challenge + wallet proof succeeded`);
|
|
2756
|
+
results.push({ test: "challenge_flow", status: "ok", ms: totalMs, walletProof: true });
|
|
2757
|
+
} catch (sigErr) {
|
|
2758
|
+
printError(` FAIL (wallet signing): ${sigErr.message}`);
|
|
2759
|
+
results.push({ test: "challenge_flow", status: "fail", ms: Date.now() - t4Start, error: sigErr.message });
|
|
2760
|
+
}
|
|
2761
|
+
} else {
|
|
2762
|
+
printWarn(` PARTIAL (${ms}ms) — challenge OK but wallet proof needed and TRADERCLAW_WALLET_PRIVATE_KEY not available`);
|
|
2763
|
+
printWarn(" Set TRADERCLAW_WALLET_PRIVATE_KEY env var or pass --wallet-private-key to test fully");
|
|
2764
|
+
results.push({ test: "challenge_flow", status: "partial", ms, walletProofRequired: true, keyAvailable: false });
|
|
2765
|
+
}
|
|
2766
|
+
} else {
|
|
2767
|
+
const tokens = await doSessionStart(orchestratorUrl, pluginConfig.apiKey, challenge.challengeId);
|
|
2768
|
+
const totalMs = Date.now() - t4Start;
|
|
2769
|
+
currentAccessToken = tokens.accessToken;
|
|
2770
|
+
currentRefreshToken = tokens.refreshToken;
|
|
2771
|
+
printSuccess(` OK (${totalMs}ms) — challenge flow succeeded (no wallet proof needed)`);
|
|
2772
|
+
results.push({ test: "challenge_flow", status: "ok", ms: totalMs, walletProof: false });
|
|
2773
|
+
}
|
|
2774
|
+
} catch (err) {
|
|
2775
|
+
printError(` FAIL: ${err.message}`);
|
|
2776
|
+
results.push({ test: "challenge_flow", status: "fail", ms: Date.now() - t4Start, error: err.message });
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
// --- Test 5: Persist rotated tokens back to sidecar ---
|
|
2780
|
+
print(" [5/5] Persist tokens to sidecar...");
|
|
2781
|
+
try {
|
|
2782
|
+
if (currentRefreshToken && currentAccessToken) {
|
|
2783
|
+
mkdirSync(dataDir, { recursive: true });
|
|
2784
|
+
const payload = {
|
|
2785
|
+
refreshToken: currentRefreshToken,
|
|
2786
|
+
accessToken: currentAccessToken,
|
|
2787
|
+
accessTokenExpiresAt: Date.now() + 900_000,
|
|
2788
|
+
walletPublicKey: pluginConfig.walletPublicKey || undefined,
|
|
2789
|
+
};
|
|
2790
|
+
const tmp = `${sessionTokensPath}.${process.pid}.${Date.now()}.tmp`;
|
|
2791
|
+
writeFileSync(tmp, JSON.stringify(payload, null, 2) + "\n", "utf-8");
|
|
2792
|
+
const { renameSync } = await import("fs");
|
|
2793
|
+
renameSync(tmp, sessionTokensPath);
|
|
2794
|
+
printSuccess(` OK — written to ${sessionTokensPath}`);
|
|
2795
|
+
results.push({ test: "persist_sidecar", status: "ok" });
|
|
2796
|
+
|
|
2797
|
+
pluginConfig.refreshToken = currentRefreshToken;
|
|
2798
|
+
removeLegacyWalletPrivateKey(pluginConfig);
|
|
2799
|
+
setPluginConfig(config, pluginConfig);
|
|
2800
|
+
writeConfig(config);
|
|
2801
|
+
printSuccess(" Config updated with latest refresh token");
|
|
2802
|
+
} else {
|
|
2803
|
+
printWarn(" SKIP — no valid tokens to persist");
|
|
2804
|
+
results.push({ test: "persist_sidecar", status: "skip" });
|
|
2805
|
+
}
|
|
2806
|
+
} catch (err) {
|
|
2807
|
+
printError(` FAIL: ${err.message}`);
|
|
2808
|
+
results.push({ test: "persist_sidecar", status: "fail", error: err.message });
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
// --- Summary ---
|
|
2812
|
+
print("\n" + "=".repeat(50));
|
|
2813
|
+
const passed = results.filter((r) => r.status === "ok").length;
|
|
2814
|
+
const failed = results.filter((r) => r.status === "fail").length;
|
|
2815
|
+
const partial = results.filter((r) => r.status === "partial" || r.status === "expired" || r.status === "skip").length;
|
|
2816
|
+
|
|
2817
|
+
if (failed === 0 && passed > 0) {
|
|
2818
|
+
printSuccess(`\n ALL TESTS PASSED (${passed}/${results.length})`);
|
|
2819
|
+
} else if (failed > 0) {
|
|
2820
|
+
printError(`\n ${failed} FAILED, ${passed} passed, ${partial} skipped/partial`);
|
|
2821
|
+
} else {
|
|
2822
|
+
printWarn(`\n ${passed} passed, ${partial} skipped/partial`);
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
print("\n Build-and-test workflow (no reinstall):");
|
|
2826
|
+
printInfo(" cd /path/to/plugin && npm run build");
|
|
2827
|
+
printInfo(" npm link");
|
|
2828
|
+
printInfo(" sudo systemctl restart openclaw");
|
|
2829
|
+
printInfo(" traderclaw test-session");
|
|
2830
|
+
print("");
|
|
2831
|
+
|
|
2832
|
+
if (failed > 0) process.exit(1);
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2599
2835
|
function printHelp() {
|
|
2600
2836
|
print(`
|
|
2601
2837
|
TraderClaw V1 CLI v${VERSION}
|
|
@@ -2612,6 +2848,7 @@ Commands:
|
|
|
2612
2848
|
logout Revoke current session and clear tokens
|
|
2613
2849
|
status Check connection health and wallet status
|
|
2614
2850
|
config View and manage configuration
|
|
2851
|
+
test-session Test session auth flow (refresh, rotation, challenge) without reinstalling
|
|
2615
2852
|
|
|
2616
2853
|
Setup options:
|
|
2617
2854
|
--api-key, -k API key (skip interactive prompt)
|
|
@@ -2658,6 +2895,8 @@ Examples:
|
|
|
2658
2895
|
traderclaw status
|
|
2659
2896
|
traderclaw config show
|
|
2660
2897
|
traderclaw config set apiTimeout 60000
|
|
2898
|
+
traderclaw test-session
|
|
2899
|
+
traderclaw test-session --wallet-private-key <base58_key>
|
|
2661
2900
|
`);
|
|
2662
2901
|
}
|
|
2663
2902
|
|
|
@@ -2703,6 +2942,9 @@ async function main() {
|
|
|
2703
2942
|
case "config":
|
|
2704
2943
|
await cmdConfig(args.slice(1));
|
|
2705
2944
|
break;
|
|
2945
|
+
case "test-session":
|
|
2946
|
+
await cmdTestSession(args.slice(1));
|
|
2947
|
+
break;
|
|
2706
2948
|
default:
|
|
2707
2949
|
printError(`Unknown command: ${command}`);
|
|
2708
2950
|
printHelp();
|
package/bin/traderclaw.cjs
CHANGED
|
File without changes
|
|
@@ -144,6 +144,8 @@ var SessionManager = class {
|
|
|
144
144
|
log;
|
|
145
145
|
refreshInFlight = null;
|
|
146
146
|
refreshTokenTtlMs = 0;
|
|
147
|
+
accessTokenTtlMs = 0;
|
|
148
|
+
tokenGeneration = 0;
|
|
147
149
|
proactiveRefreshTimer = null;
|
|
148
150
|
proactiveRefreshRunning = false;
|
|
149
151
|
constructor(config) {
|
|
@@ -224,6 +226,7 @@ var SessionManager = class {
|
|
|
224
226
|
if (!this.refreshTokenValue) {
|
|
225
227
|
throw new Error("No refresh token available. Must authenticate via challenge flow.");
|
|
226
228
|
}
|
|
229
|
+
const genBefore = this.tokenGeneration;
|
|
227
230
|
const res = await rawFetch(
|
|
228
231
|
`${this.baseUrl}/api/session/refresh`,
|
|
229
232
|
"POST",
|
|
@@ -233,9 +236,13 @@ var SessionManager = class {
|
|
|
233
236
|
);
|
|
234
237
|
if (!res.ok) {
|
|
235
238
|
if (res.status === 401 || res.status === 403) {
|
|
236
|
-
this.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
+
if (this.tokenGeneration === genBefore) {
|
|
240
|
+
this.accessToken = null;
|
|
241
|
+
this.refreshTokenValue = null;
|
|
242
|
+
this.accessTokenExpiresAt = 0;
|
|
243
|
+
} else {
|
|
244
|
+
this.log.info("[session] Stale 401/403 ignored \u2014 tokens already rotated by concurrent call.");
|
|
245
|
+
}
|
|
239
246
|
throw new Error("Refresh token expired or revoked. Must re-authenticate via challenge flow.");
|
|
240
247
|
}
|
|
241
248
|
throw new Error(`Token refresh failed (HTTP ${res.status}): ${JSON.stringify(res.data)}`);
|
|
@@ -303,14 +310,7 @@ var SessionManager = class {
|
|
|
303
310
|
if (this.accessToken && Date.now() < this.accessTokenExpiresAt - 12e4) {
|
|
304
311
|
return this.accessToken;
|
|
305
312
|
}
|
|
306
|
-
|
|
307
|
-
this.refreshInFlight = this.ensureRefreshed();
|
|
308
|
-
}
|
|
309
|
-
try {
|
|
310
|
-
await this.refreshInFlight;
|
|
311
|
-
} finally {
|
|
312
|
-
this.refreshInFlight = null;
|
|
313
|
-
}
|
|
313
|
+
await this.unifiedRefresh();
|
|
314
314
|
if (!this.accessToken) {
|
|
315
315
|
throw new Error(
|
|
316
316
|
`Session expired and could not be refreshed. Re-authentication required. Troubleshooting: ${TRADERCLAW_SESSION_TROUBLESHOOTING}`
|
|
@@ -321,14 +321,7 @@ var SessionManager = class {
|
|
|
321
321
|
async handleUnauthorized() {
|
|
322
322
|
this.accessToken = null;
|
|
323
323
|
this.accessTokenExpiresAt = 0;
|
|
324
|
-
|
|
325
|
-
this.refreshInFlight = this.ensureRefreshed();
|
|
326
|
-
}
|
|
327
|
-
try {
|
|
328
|
-
await this.refreshInFlight;
|
|
329
|
-
} finally {
|
|
330
|
-
this.refreshInFlight = null;
|
|
331
|
-
}
|
|
324
|
+
await this.unifiedRefresh();
|
|
332
325
|
if (!this.accessToken) {
|
|
333
326
|
throw new Error(
|
|
334
327
|
`Session expired and could not be refreshed. Re-authentication required. Troubleshooting: ${TRADERCLAW_SESSION_TROUBLESHOOTING}`
|
|
@@ -336,6 +329,20 @@ var SessionManager = class {
|
|
|
336
329
|
}
|
|
337
330
|
return this.accessToken;
|
|
338
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* Single-mutex refresh: all paths (proactive timer, on-demand getAccessToken,
|
|
334
|
+
* handleUnauthorized) funnel through here so only one refresh HTTP call is
|
|
335
|
+
* ever in-flight. Prevents the race where two concurrent refresh() calls
|
|
336
|
+
* with the same rotating refresh token kill the session.
|
|
337
|
+
*/
|
|
338
|
+
async unifiedRefresh() {
|
|
339
|
+
if (!this.refreshInFlight) {
|
|
340
|
+
this.refreshInFlight = this.ensureRefreshed().finally(() => {
|
|
341
|
+
this.refreshInFlight = null;
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
await this.refreshInFlight;
|
|
345
|
+
}
|
|
339
346
|
isAuthenticated() {
|
|
340
347
|
return !!this.accessToken;
|
|
341
348
|
}
|
|
@@ -357,9 +364,11 @@ var SessionManager = class {
|
|
|
357
364
|
return this.walletPublicKey;
|
|
358
365
|
}
|
|
359
366
|
applyTokens(tokens) {
|
|
367
|
+
this.tokenGeneration++;
|
|
360
368
|
this.accessToken = tokens.accessToken;
|
|
361
369
|
this.refreshTokenValue = tokens.refreshToken;
|
|
362
370
|
this.accessTokenExpiresAt = Date.now() + tokens.accessTokenTtlSeconds * 1e3;
|
|
371
|
+
this.accessTokenTtlMs = tokens.accessTokenTtlSeconds * 1e3;
|
|
363
372
|
this.refreshTokenTtlMs = (tokens.refreshTokenTtlSeconds || 0) * 1e3;
|
|
364
373
|
this.sessionId = tokens.session.id;
|
|
365
374
|
this.tier = tokens.session.tier;
|
|
@@ -375,14 +384,19 @@ var SessionManager = class {
|
|
|
375
384
|
this.scheduleProactiveRefresh();
|
|
376
385
|
}
|
|
377
386
|
/**
|
|
378
|
-
* Schedule a background token refresh
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
387
|
+
* Schedule a repeating background token refresh. Uses setInterval so the
|
|
388
|
+
* chain cannot silently break if a single cycle fails to re-schedule.
|
|
389
|
+
*
|
|
390
|
+
* Interval = min(50% refresh-token TTL, accessTokenTtl - 2.5 min buffer),
|
|
391
|
+
* clamped between 2 min and 20 min. Falls back to 10 min when TTLs unknown.
|
|
392
|
+
*
|
|
393
|
+
* Goes through unifiedRefresh() so it shares the same mutex as on-demand
|
|
394
|
+
* callers and can fall back to the full challenge flow when refresh tokens
|
|
395
|
+
* are permanently revoked.
|
|
382
396
|
*/
|
|
383
397
|
scheduleProactiveRefresh() {
|
|
384
398
|
if (this.proactiveRefreshTimer) {
|
|
385
|
-
|
|
399
|
+
clearInterval(this.proactiveRefreshTimer);
|
|
386
400
|
this.proactiveRefreshTimer = null;
|
|
387
401
|
}
|
|
388
402
|
const MIN_INTERVAL_MS = 2 * 60 * 1e3;
|
|
@@ -394,17 +408,20 @@ var SessionManager = class {
|
|
|
394
408
|
} else {
|
|
395
409
|
intervalMs = DEFAULT_INTERVAL_MS;
|
|
396
410
|
}
|
|
397
|
-
this.
|
|
411
|
+
if (this.accessTokenTtlMs > 0) {
|
|
412
|
+
const accessBasedMs = Math.max(MIN_INTERVAL_MS, this.accessTokenTtlMs - 15e4);
|
|
413
|
+
intervalMs = Math.min(intervalMs, accessBasedMs);
|
|
414
|
+
}
|
|
415
|
+
this.log.info(`[session] Proactive refresh scheduled every ${Math.round(intervalMs / 1e3)}s`);
|
|
416
|
+
this.proactiveRefreshTimer = setInterval(async () => {
|
|
398
417
|
if (this.proactiveRefreshRunning) return;
|
|
399
418
|
this.proactiveRefreshRunning = true;
|
|
400
419
|
try {
|
|
401
|
-
if (!this.refreshTokenValue) return;
|
|
402
420
|
this.log.info(`[session] Proactive token refresh (interval: ${Math.round(intervalMs / 1e3)}s)...`);
|
|
403
|
-
await this.
|
|
421
|
+
await this.unifiedRefresh();
|
|
404
422
|
this.log.info("[session] Proactive refresh succeeded \u2014 token chain extended.");
|
|
405
423
|
} catch (err) {
|
|
406
|
-
this.log.warn(`[session] Proactive refresh failed: ${err.message}. Will retry next
|
|
407
|
-
this.scheduleProactiveRefresh();
|
|
424
|
+
this.log.warn(`[session] Proactive refresh failed: ${err.message}. Will retry next interval or on-demand.`);
|
|
408
425
|
} finally {
|
|
409
426
|
this.proactiveRefreshRunning = false;
|
|
410
427
|
}
|
|
@@ -415,7 +432,7 @@ var SessionManager = class {
|
|
|
415
432
|
}
|
|
416
433
|
destroy() {
|
|
417
434
|
if (this.proactiveRefreshTimer) {
|
|
418
|
-
|
|
435
|
+
clearInterval(this.proactiveRefreshTimer);
|
|
419
436
|
this.proactiveRefreshTimer = null;
|
|
420
437
|
}
|
|
421
438
|
}
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from "./chunk-T4YWGIIR.js";
|
|
10
10
|
import {
|
|
11
11
|
SessionManager
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-GYXPEC7O.js";
|
|
13
13
|
|
|
14
14
|
// index.ts
|
|
15
15
|
import { Type } from "@sinclair/typebox";
|
|
@@ -1254,14 +1254,31 @@ var solanaTraderPlugin = {
|
|
|
1254
1254
|
});
|
|
1255
1255
|
api.registerTool({
|
|
1256
1256
|
name: "solana_trade_precheck",
|
|
1257
|
-
description: "Pre-trade risk check \u2014 validates a proposed trade against risk rules, kill switch, entitlement limits, and on-chain conditions. Returns approved/denied with reasons and capped size. Always call this before executing a trade. Buy: sizeSol required; do not send sizeTokens or sellPct. Sell: send exactly one of sizeTokens or sellPct (not sizeSol). If both sellPct and sizeTokens are sent, sellPct is preferred and sizeTokens is ignored.",
|
|
1257
|
+
description: "Pre-trade risk check \u2014 validates a proposed trade against risk rules, kill switch, entitlement limits, and on-chain conditions. Returns approved/denied with reasons and capped size. Always call this before executing a trade. Buy: sizeSol required; do not send sizeTokens or sellPct. Sell: send exactly one of sizeTokens or sellPct (not sizeSol). If both sellPct and sizeTokens are sent, sellPct is preferred and sizeTokens is ignored. Optional exit fields (trailingStopPct, trailingStop) are accepted to mirror execute payloads; sizing logic ignores them.",
|
|
1258
1258
|
parameters: Type.Object({
|
|
1259
1259
|
tokenAddress: Type.String({ description: "Solana token mint address" }),
|
|
1260
1260
|
side: Type.Union([Type.Literal("buy"), Type.Literal("sell")], { description: "Trade direction" }),
|
|
1261
1261
|
sizeSol: Type.Optional(Type.Number({ description: "Position size in SOL \u2014 required for buy, omit for sell" })),
|
|
1262
1262
|
sellPct: Type.Optional(Type.Number({ description: "Sell percentage 1\u2013100 (100 = full exit) \u2014 sell only; preferred over sizeTokens if both sent" })),
|
|
1263
1263
|
sizeTokens: Type.Optional(Type.Number({ description: "Token amount to sell \u2014 sell only; ignored if sellPct is also provided" })),
|
|
1264
|
-
slippageBps: Type.Optional(Type.Number({ description: "Slippage tolerance in basis points (e.g., 300 = 3%)" }))
|
|
1264
|
+
slippageBps: Type.Optional(Type.Number({ description: "Slippage tolerance in basis points (e.g., 300 = 3%)" })),
|
|
1265
|
+
trailingStopPct: Type.Optional(Type.Number({ description: "Optional \u2014 same as execute; ignored for policy sizing" })),
|
|
1266
|
+
trailingStop: Type.Optional(
|
|
1267
|
+
Type.Object({
|
|
1268
|
+
levels: Type.Array(
|
|
1269
|
+
Type.Object({
|
|
1270
|
+
percentage: Type.Number({ description: "Trailing drawdown % from the armed high once the level is active" }),
|
|
1271
|
+
amount: Type.Optional(Type.Number({ description: "% of position to sell at this level (1\u2013100). Server default 100." })),
|
|
1272
|
+
triggerAboveATH: Type.Optional(
|
|
1273
|
+
Type.Number({
|
|
1274
|
+
description: "Optional. % above session ATH before this level arms. If omitted, API defaults to 100 (2\xD7 ATH)."
|
|
1275
|
+
})
|
|
1276
|
+
)
|
|
1277
|
+
}),
|
|
1278
|
+
{ minItems: 1, maxItems: 5, description: "Multi-level trailing (optional on precheck)" }
|
|
1279
|
+
)
|
|
1280
|
+
})
|
|
1281
|
+
)
|
|
1265
1282
|
}),
|
|
1266
1283
|
execute: wrapExecute(async (_id, params) => {
|
|
1267
1284
|
const body = {
|
|
@@ -1269,6 +1286,13 @@ var solanaTraderPlugin = {
|
|
|
1269
1286
|
side: params.side,
|
|
1270
1287
|
slippageBps: params.slippageBps
|
|
1271
1288
|
};
|
|
1289
|
+
if (params.trailingStopPct !== void 0) {
|
|
1290
|
+
body.trailingStopPct = params.trailingStopPct;
|
|
1291
|
+
}
|
|
1292
|
+
const ts = params.trailingStop;
|
|
1293
|
+
if (ts?.levels && Array.isArray(ts.levels) && ts.levels.length > 0) {
|
|
1294
|
+
body.trailingStop = ts;
|
|
1295
|
+
}
|
|
1272
1296
|
if (params.side === "buy") {
|
|
1273
1297
|
body.sizeSol = params.sizeSol;
|
|
1274
1298
|
} else {
|
|
@@ -1283,7 +1307,7 @@ var solanaTraderPlugin = {
|
|
|
1283
1307
|
});
|
|
1284
1308
|
api.registerTool({
|
|
1285
1309
|
name: "solana_trade_execute",
|
|
1286
|
-
description: "Execute a trade on Solana via the SpyFly bot. Enforces risk rules before proxying to on-chain execution. Returns trade ID, position ID, and transaction signature. IMPORTANT: tpLevels alone (e.g. [10, 15]) means EACH level sells 100% of the position at that gain \u2014 use tpExits for partials (e.g. +10% sell 50%, +15% sell 100%). Buy: sizeSol required; do not send sizeTokens or sellPct. Sell: send exactly one of sizeTokens or sellPct (not sizeSol). If both sellPct and sizeTokens are sent, sellPct is preferred and sizeTokens is ignored.",
|
|
1310
|
+
description: "Execute a trade on Solana via the SpyFly bot. Enforces risk rules before proxying to on-chain execution. Returns trade ID, position ID, and transaction signature. IMPORTANT: tpLevels alone (e.g. [10, 15]) means EACH level sells 100% of the position at that gain \u2014 use tpExits for partials (e.g. +10% sell 50%, +15% sell 100%). Trailing: use `trailingStopPct` for a single simple trailing %, or `trailingStop.levels` (1\u20135) for multi-level trailing with optional `triggerAboveATH` per level (% above session ATH before that level arms; if omitted, server defaults to 100 i.e. 2\xD7 ATH). When both are sent, `trailingStop` wins. Buy: sizeSol required; do not send sizeTokens or sellPct. Sell: send exactly one of sizeTokens or sellPct (not sizeSol). If both sellPct and sizeTokens are sent, sellPct is preferred and sizeTokens is ignored.",
|
|
1287
1311
|
parameters: Type.Object({
|
|
1288
1312
|
tokenAddress: Type.String({ description: "Solana token mint address" }),
|
|
1289
1313
|
side: Type.Union([Type.Literal("buy"), Type.Literal("sell")], { description: "Trade direction" }),
|
|
@@ -1318,7 +1342,37 @@ var solanaTraderPlugin = {
|
|
|
1318
1342
|
{ description: "Multi-level stop-loss with partial exits (optional). Otherwise use slPct for a single full exit." }
|
|
1319
1343
|
)
|
|
1320
1344
|
),
|
|
1321
|
-
trailingStopPct: Type.Optional(
|
|
1345
|
+
trailingStopPct: Type.Optional(
|
|
1346
|
+
Type.Number({
|
|
1347
|
+
description: "Single trailing-stop % (legacy). Ignored if `trailingStop` is provided."
|
|
1348
|
+
})
|
|
1349
|
+
),
|
|
1350
|
+
trailingStop: Type.Optional(
|
|
1351
|
+
Type.Object({
|
|
1352
|
+
levels: Type.Array(
|
|
1353
|
+
Type.Object({
|
|
1354
|
+
percentage: Type.Number({
|
|
1355
|
+
description: "Once armed, sell when price drops this % from the high (trailing drawdown)."
|
|
1356
|
+
}),
|
|
1357
|
+
amount: Type.Optional(
|
|
1358
|
+
Type.Number({
|
|
1359
|
+
description: "% of position to sell when this level fires (1\u2013100). Server default 100."
|
|
1360
|
+
})
|
|
1361
|
+
),
|
|
1362
|
+
triggerAboveATH: Type.Optional(
|
|
1363
|
+
Type.Number({
|
|
1364
|
+
description: "Optional. Session price must reach this % above session ATH before this level arms (e.g. 50 \u2192 1.5\xD7 ATH). If omitted, API defaults to 100 (2\xD7 ATH)."
|
|
1365
|
+
})
|
|
1366
|
+
)
|
|
1367
|
+
}),
|
|
1368
|
+
{
|
|
1369
|
+
minItems: 1,
|
|
1370
|
+
maxItems: 5,
|
|
1371
|
+
description: "Ordered trailing-stop levels (up to 5)."
|
|
1372
|
+
}
|
|
1373
|
+
)
|
|
1374
|
+
})
|
|
1375
|
+
),
|
|
1322
1376
|
managementMode: Type.Optional(
|
|
1323
1377
|
Type.Union([Type.Literal("LOCAL_MANAGED"), Type.Literal("SERVER_MANAGED")], {
|
|
1324
1378
|
description: "Advisory only \u2014 server decides position mode internally. Sent for future compatibility."
|
|
@@ -1337,9 +1391,14 @@ var solanaTraderPlugin = {
|
|
|
1337
1391
|
symbol: params.symbol,
|
|
1338
1392
|
slippageBps: params.slippageBps,
|
|
1339
1393
|
slPct: params.slPct,
|
|
1340
|
-
trailingStopPct: params.trailingStopPct,
|
|
1341
1394
|
managementMode: params.managementMode
|
|
1342
1395
|
};
|
|
1396
|
+
const tsExecute = params.trailingStop;
|
|
1397
|
+
if (tsExecute?.levels && Array.isArray(tsExecute.levels) && tsExecute.levels.length > 0) {
|
|
1398
|
+
body.trailingStop = tsExecute;
|
|
1399
|
+
} else if (params.trailingStopPct !== void 0) {
|
|
1400
|
+
body.trailingStopPct = params.trailingStopPct;
|
|
1401
|
+
}
|
|
1343
1402
|
if (params.side === "buy") {
|
|
1344
1403
|
body.sizeSol = params.sizeSol;
|
|
1345
1404
|
} else {
|
|
@@ -1507,7 +1566,7 @@ var solanaTraderPlugin = {
|
|
|
1507
1566
|
});
|
|
1508
1567
|
api.registerTool({
|
|
1509
1568
|
name: "solana_capital_status",
|
|
1510
|
-
description: "Get your current capital status \u2014 SOL balance, open position count, unrealized PnL, daily notional used, daily loss, and effective limits (
|
|
1569
|
+
description: "Get your current capital status \u2014 SOL balance, open position count, unrealized/realized PnL, daily notional used, daily loss, and effective limits. **PnL:** `totalUnrealizedPnl` / `totalRealizedPnl` are USD (DB); use `totalUnrealizedPnlSol` / `totalRealizedPnlSol` / `totalPnlSol` for SOL (derived via `solPriceUsd`, same as positions API).",
|
|
1511
1570
|
parameters: Type.Object({}),
|
|
1512
1571
|
execute: wrapExecute(
|
|
1513
1572
|
async () => get(`/api/capital/status?walletId=${walletId}`)
|
|
@@ -1515,7 +1574,7 @@ var solanaTraderPlugin = {
|
|
|
1515
1574
|
});
|
|
1516
1575
|
api.registerTool({
|
|
1517
1576
|
name: "solana_positions",
|
|
1518
|
-
description: "List
|
|
1577
|
+
description: "List trading positions with mark-to-market. **PnL:** `realizedPnl` / `unrealizedPnl` are USD as stored; prefer `realizedPnlSol` / `unrealizedPnlSol` when reasoning in SOL. `unrealizedReturnPct` is ROI on cost basis (for sweep-dead-tokens logic). See response `pnlNote`.",
|
|
1519
1578
|
parameters: Type.Object({
|
|
1520
1579
|
status: Type.Optional(Type.String({ description: "Filter by status: 'open', 'closed', or omit for all" }))
|
|
1521
1580
|
}),
|
|
@@ -1525,6 +1584,38 @@ var solanaTraderPlugin = {
|
|
|
1525
1584
|
return get(path2);
|
|
1526
1585
|
})
|
|
1527
1586
|
});
|
|
1587
|
+
api.registerTool({
|
|
1588
|
+
name: "solana_wallet_token_balance",
|
|
1589
|
+
description: "Read on-chain SPL token balance (UI amount) for your trading wallet and a token mint. Same balance path as server exit monitoring (`balanceOf`).",
|
|
1590
|
+
parameters: Type.Object({
|
|
1591
|
+
tokenAddress: Type.String({ description: "SPL token mint address" })
|
|
1592
|
+
}),
|
|
1593
|
+
execute: wrapExecute(
|
|
1594
|
+
async (_id, params) => post("/api/wallet/token-balance", {
|
|
1595
|
+
walletId,
|
|
1596
|
+
tokenAddress: params.tokenAddress
|
|
1597
|
+
})
|
|
1598
|
+
)
|
|
1599
|
+
});
|
|
1600
|
+
api.registerTool({
|
|
1601
|
+
name: "solana_sweep_dead_tokens",
|
|
1602
|
+
description: "Sell 100% of each OPEN position whose unrealizedReturnPct is \u2264 -maxLossPct (default 80), using the same mark-to-market as positions. Use dryRun:true first to list candidates. Executes sequential full exits (sellPct 100). Requires trade:execute scope.",
|
|
1603
|
+
parameters: Type.Object({
|
|
1604
|
+
maxLossPct: Type.Optional(
|
|
1605
|
+
Type.Number({ description: "Threshold: sweep when unrealizedReturnPct <= -maxLossPct (default 80)" })
|
|
1606
|
+
),
|
|
1607
|
+
slippageBps: Type.Optional(Type.Number({ description: "Per-exit slippage in bps (default 300)" })),
|
|
1608
|
+
dryRun: Type.Optional(Type.Boolean({ description: "If true, only return candidate tokens without selling" }))
|
|
1609
|
+
}),
|
|
1610
|
+
execute: wrapExecute(
|
|
1611
|
+
async (_id, params) => post("/api/wallet/sweep-dead-tokens", {
|
|
1612
|
+
walletId,
|
|
1613
|
+
maxLossPct: params.maxLossPct,
|
|
1614
|
+
slippageBps: params.slippageBps,
|
|
1615
|
+
dryRun: params.dryRun
|
|
1616
|
+
})
|
|
1617
|
+
)
|
|
1618
|
+
});
|
|
1528
1619
|
api.registerTool({
|
|
1529
1620
|
name: "solana_funding_instructions",
|
|
1530
1621
|
description: "Get deposit instructions for funding your trading wallet with SOL.",
|
package/package.json
CHANGED
|
@@ -14,7 +14,7 @@ The orchestrator gathers data, enforces execution policy, applies entitlement li
|
|
|
14
14
|
|
|
15
15
|
## How You Access the Orchestrator
|
|
16
16
|
|
|
17
|
-
You interact with the orchestrator **exclusively through plugin tools** (e.g. `solana_system_status`, `
|
|
17
|
+
You interact with the orchestrator **exclusively through plugin tools** (e.g. `solana_system_status`, `solana_scan_hot_pairs`,`solana_scan_launches` , `solana_alpha_signals`, `solana_trade_precheck`, `solana_trade_execute`, etc.). You have no other access method.
|
|
18
18
|
|
|
19
19
|
**Critical rules:**
|
|
20
20
|
- **You do NOT have direct HTTP/API access.** Never attempt to call REST endpoints, use curl/fetch, or construct API URLs. You cannot reach the orchestrator that way.
|
|
@@ -234,8 +234,14 @@ All values are internal targets that must still comply with server policy caps.
|
|
|
234
234
|
| Position size (exploratory) | 3–8% of capital | 5–10% of capital |
|
|
235
235
|
| Max correlated cluster exposure | 40% of capital | 40% of capital |
|
|
236
236
|
| Consecutive losses → kill switch | 5 | 7 |
|
|
237
|
-
|
|
|
238
|
-
|
|
|
237
|
+
| Stop loss (slExits) | -20% on every position | -40% on every position |
|
|
238
|
+
| Trailing Stop loss ("trailingStop": {
|
|
239
|
+
"levels": [
|
|
240
|
+
{ "percentage": 25, "amount": 50 },
|
|
241
|
+
{ "percentage": 35, "amount": 100, "triggerAboveATH": 100 }
|
|
242
|
+
]
|
|
243
|
+
}) where percent is the price decrease from entry price and amount is the size of position in percentage and in token holding | -20% on every position and optional triggerAboveATH | -40% on every position |
|
|
244
|
+
| Multiple Take Profit exits (tpExits) (e.g [{"percent": 100, "amountPct": 30},{ "percent": 200, "amountPct": 100" }], where percent is the price increase from entry price and amountPct is the size of position in percentage and in token holding) | +100–300% (multiple) | +200–500% (multiple) |
|
|
239
245
|
| Exploration ratio | 20% experimental / 80% proven | 50% experimental / 50% proven |
|
|
240
246
|
| Weight evolution (minimum trades) | ≥20 closed trades | ≥20 closed trades |
|
|
241
247
|
| Max weight delta per update | ±0.10 | ±0.15 |
|
|
@@ -330,7 +336,7 @@ There is no context loss from this separation. Cron outputs flow into the persis
|
|
|
330
336
|
↓
|
|
331
337
|
2. Step 0: INTERRUPT CHECK — identify wake-up trigger, check kill switch, check dead money, STRATEGY INTEGRITY CHECK
|
|
332
338
|
↓
|
|
333
|
-
3. Step 1: SCAN — call
|
|
339
|
+
3. Step 1: SCAN — call solana_scan_launches and solana_scan_hot_pairs for broad discovery, process Bitquery subscriptions
|
|
334
340
|
↓
|
|
335
341
|
4. Step 1.5b: ALPHA SIGNALS — poll solana_alpha_signals, score, classify priority
|
|
336
342
|
↓
|
|
@@ -902,10 +908,6 @@ Discovery subscriptions complement — not replace — scan endpoints and alpha
|
|
|
902
908
|
|
|
903
909
|
When multiple paths independently surface the same token = convergence = highest conviction. Log convergence events via `solana_memory_write` with tag `signal_convergence`.
|
|
904
910
|
|
|
905
|
-
**Future enhancement (not yet available):**
|
|
906
|
-
|
|
907
|
-
When `solana_firehose_config` and `solana_firehose_status` tools become available, you will be able to configure advanced filter parameters (volume thresholds, buyer counts, whale detection) directly on the orchestrator or local worker, and check health/stats. For now, use the existing `solana_bitquery_subscribe` with the available template keys and evolve your subscription strategy based on outcomes.
|
|
908
|
-
|
|
909
911
|
---
|
|
910
912
|
|
|
911
913
|
### Step 2: ANALYZE — Deep Dive
|
|
@@ -1128,9 +1130,11 @@ If BUY, you must define before executing:
|
|
|
1128
1130
|
- `tpLevels` — staged take-profit levels as percentages
|
|
1129
1131
|
- HARDENED: `[50, 100, 200]` (patient, ride trends)
|
|
1130
1132
|
- DEGEN: `[25, 50, 100]` (lock gains faster)
|
|
1131
|
-
- `trailingStopPct` — trailing stop
|
|
1132
|
-
|
|
1133
|
-
-
|
|
1133
|
+
- `trailingStopPct` — single trailing stop (% drawdown from session high once active). Legacy/simple path on the server.
|
|
1134
|
+
- **`trailingStop`** (preferred for staging) — `{ levels: [ ... ] }` with **1–5** levels. Each level:
|
|
1135
|
+
- `percentage` — trailing drawdown % from the armed high once that level is active (required).
|
|
1136
|
+
- `amount` — % of position to sell at this level (1–100; server default **100**).
|
|
1137
|
+
- `triggerAboveATH` — **optional.** Price must reach this **% above the session ATH** before this level arms (e.g. `50` → 1.5× ATH). **If omitted, the API defaults to `100` (2× ATH).** Use a smaller value (e.g. `25`) to arm earlier; use `trailingStopPct` instead if you want the simpler single-level trailing that keys off ATH without this gate.
|
|
1134
1138
|
- `managementMode` — LOCAL_MANAGED or SERVER_MANAGED
|
|
1135
1139
|
- `slippageBps` — must scale with liquidity:
|
|
1136
1140
|
- Pool depth > $500K: 100–200 bps
|
|
@@ -1227,7 +1231,7 @@ Call `solana_trade_execute` with:
|
|
|
1227
1231
|
- **For buy:** `sizeSol` (amount in SOL to spend) — required
|
|
1228
1232
|
- **For sell:** `sellPct` (percentage of position to sell, 1–100 where 100 = full exit) **or** `sizeTokens` (exact token count) — one is required. If both sent, `sellPct` wins. Do NOT send `sizeSol` for sells.
|
|
1229
1233
|
- `slippageBps` (scaled to liquidity as defined in your exit plan — hard cap 800bps)
|
|
1230
|
-
- `slPct`, `tpLevels`,
|
|
1234
|
+
- `slPct`, `tpLevels`, **`trailingStopPct` and/or `trailingStop`** (see **Define Exit Plan** above — if both are sent, `trailingStop` wins)
|
|
1231
1235
|
- `managementMode`
|
|
1232
1236
|
|
|
1233
1237
|
Record the returned `tradeId` and `positionId`. You will need these for monitoring and review.
|
|
@@ -1669,7 +1673,6 @@ Your discovery subscriptions (Step 1.75) should evolve alongside your strategy w
|
|
|
1669
1673
|
- **Adjusting discovery subscriptions** (broad, for token detection):
|
|
1670
1674
|
Discovery subscriptions like `pumpFunTokenCreation` and `raydiumNewPools` are fire-and-forget — you set them up once and they run until you unsubscribe. Evolution here means deciding which broad subscriptions to keep vs remove, not changing their parameters.
|
|
1671
1675
|
|
|
1672
|
-
When `solana_firehose_config` becomes available, you will be able to configure advanced filter parameters (volume thresholds, buyer counts, whale detection) on the orchestrator or local worker side without the unsub/resub cycle.
|
|
1673
1676
|
|
|
1674
1677
|
5. Mode switches should trigger subscription review:
|
|
1675
1678
|
- Switching to DEGEN → add more discovery subscriptions, broaden parameters
|
|
@@ -2064,8 +2067,10 @@ All authenticated endpoints use `Authorization: Bearer <accessToken>`.
|
|
|
2064
2067
|
| `POST` | `/api/session/logout` | `refreshToken` | No auth needed. Revokes session |
|
|
2065
2068
|
| `GET` | `/api/wallets` | — | List all wallets. Optional `?refresh=true` |
|
|
2066
2069
|
| `POST` | `/api/wallet/create` | — | Create wallet. Optional: `label`, `publicKey`, `chain` (solana/bsc), `ownerRef`, `includePrivateKey`. Status `201` |
|
|
2067
|
-
| `GET` | `/api/capital/status` | `?walletId=<uuid>` | Wallet capital and daily limits |
|
|
2068
|
-
| `GET` | `/api/wallet/positions` | `?walletId=<uuid>` |
|
|
2070
|
+
| `GET` | `/api/capital/status` | `?walletId=<uuid>` | Wallet capital and daily limits. **PnL:** `totalUnrealizedPnl` / `totalRealizedPnl` are **USD** (stored); `totalUnrealizedPnlSol` / `totalRealizedPnlSol` / `totalPnlSol` are **SOL** derived with `solPriceUsd` (agent-safe). |
|
|
2071
|
+
| `GET` | `/api/wallet/positions` | `?walletId=<uuid>` | Positions. **`realizedPnl` / `unrealizedPnl` = USD** in DB; use **`realizedPnlSol` / `unrealizedPnlSol`** for SOL. `unrealizedReturnPct` = ROI vs cost (for sweep). |
|
|
2072
|
+
| `POST` | `/api/wallet/token-balance` | `walletId`, `tokenAddress` | On-chain SPL **uiAmount** for the wallet (same as server `balanceOf`) |
|
|
2073
|
+
| `POST` | `/api/wallet/sweep-dead-tokens` | `walletId` | Optional: `maxLossPct` (default **80**), `slippageBps`, `dryRun`. Sells **100%** of each **open** position with `unrealizedReturnPct` ≤ **-maxLossPct**. **trade:execute** scope. |
|
|
2069
2074
|
| `GET` | `/api/funding/instructions` | `?walletId=<uuid>` | Deposit instructions |
|
|
2070
2075
|
| `GET` | `/api/killswitch/status` | `?walletId=<uuid>` | Kill switch state |
|
|
2071
2076
|
| `POST` | `/api/killswitch` | `walletId`, `enabled` | Toggle kill switch. Optional: `mode` (TRADES_ONLY / TRADES_AND_STREAMS). **Pro tier required** |
|
|
@@ -2073,7 +2078,7 @@ All authenticated endpoints use `Authorization: Bearer <accessToken>`.
|
|
|
2073
2078
|
| `POST` | `/api/strategy/update` | `walletId`, `featureWeights` | Update weights. Optional: `strategyVersion`, `mode` (HARDENED/DEGEN) |
|
|
2074
2079
|
| `POST` | `/api/thesis/build` | `walletId`, `tokenAddress` | Build full thesis package |
|
|
2075
2080
|
| `POST` | `/api/trade/precheck` | `walletId`, `tokenAddress`, `side` (buy/sell), `slippageBps`. Buy: `sizeSol`. Sell: `sellPct` or `sizeTokens` | Risk/policy check, no execution |
|
|
2076
|
-
| `POST` | `/api/trade/execute` | `walletId`, `tokenAddress`, `side`, `slippageBps`. Buy: `sizeSol`. Sell: `sellPct` or `sizeTokens` | Execute trade. Optional: `symbol`, `tpLevels[]`, `slPct`, `trailingStopPct
|
|
2081
|
+
| `POST` | `/api/trade/execute` | `walletId`, `tokenAddress`, `side`, `slippageBps`. Buy: `sizeSol`. Sell: `sellPct` or `sizeTokens` | Execute trade. Optional: `symbol`, `tpLevels[]`, `slPct`, `trailingStopPct`, `trailingStop: { levels: [{ percentage, amount?, triggerAboveATH? }] }` (1–5 levels; **omitted `triggerAboveATH` defaults to 100** = 2× ATH). Header: `x-idempotency-key` |
|
|
2077
2082
|
| `POST` | `/api/trade/review` | `walletId`, `outcome` (win/loss/neutral), `notes` | Post-trade review. Optional: `tradeId`, `tokenAddress`, `pnlSol`, `tags[]`, `strategyVersion` (strict semver). Status `201` |
|
|
2078
2083
|
| `POST` | `/api/memory/write` | `walletId`, `notes` | Journal entry. Optional: `tokenAddress`, `outcome` (win/loss/neutral), `tags[]`, `strategyVersion` (strict semver). Status `201` |
|
|
2079
2084
|
| `POST` | `/api/memory/search` | `walletId`, `query` | Search memory entries |
|
|
@@ -2701,8 +2706,10 @@ Each user configures their own X/Twitter API developer account tokens in the plu
|
|
|
2701
2706
|
| Thesis | `solana_build_thesis` | Full context package |
|
|
2702
2707
|
| Trade | `solana_trade_precheck` | Pre-trade risk validation |
|
|
2703
2708
|
| Trade | `solana_trade_execute` | Execute trade |
|
|
2704
|
-
| Monitor | `solana_positions` | Position tracking |
|
|
2705
|
-
| Monitor | `solana_capital_status` | Portfolio status |
|
|
2709
|
+
| Monitor | `solana_positions` | Position tracking (USD PnL in DB; use `*PnlSol` fields for SOL) |
|
|
2710
|
+
| Monitor | `solana_capital_status` | Portfolio status (same USD vs SOL split as API) |
|
|
2711
|
+
| Risk | `solana_sweep_dead_tokens` | Full-exit open positions below **-maxLossPct** ROI (default 80%); use `dryRun` first |
|
|
2712
|
+
| Wallet | `solana_wallet_token_balance` | On-chain SPL balance for a mint |
|
|
2706
2713
|
| Review | `solana_trade_review` | Post-trade review |
|
|
2707
2714
|
| Memory | `solana_memory_write` | Write journal entry (deployer profiles, filter evolution, convergence) |
|
|
2708
2715
|
| Memory | `solana_memory_search` | Search memories (check deployer history before profiling) |
|