From 6dfc1313d771b156cb18c381fa6ee69a7a8d069d Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Tue, 17 Feb 2026 22:06:14 +0700 Subject: [PATCH 01/70] Create agentShared.js --- src/agents/agentShared.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/agents/agentShared.js diff --git a/src/agents/agentShared.js b/src/agents/agentShared.js new file mode 100644 index 0000000..71f3782 --- /dev/null +++ b/src/agents/agentShared.js @@ -0,0 +1,15 @@ +export function banner(title) { + const line = "═".repeat(Math.max(10, title.length + 6)); + return `\n${line}\n ${title}\n${line}\n`; +} + +export function sleep(ms) { + return new Promise((r) => setTimeout(r, ms)); +} + +export function fmt(n, d = 6) { + if (n === undefined || n === null) return "-"; + const x = typeof n === "string" ? Number(n) : n; + if (Number.isNaN(x)) return String(n); + return x.toFixed(d); +} From 862ed547aca01f9ab9ecda2cc5a761a6a169b9ad Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Tue, 17 Feb 2026 22:07:36 +0700 Subject: [PATCH 02/70] Create agentScout.js --- src/agents/agentScout.js | 54 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 src/agents/agentScout.js diff --git a/src/agents/agentScout.js b/src/agents/agentScout.js new file mode 100644 index 0000000..2539c5c --- /dev/null +++ b/src/agents/agentScout.js @@ -0,0 +1,54 @@ +import { banner, sleep, fmt } from "./agentShared.js"; + +export async function agentScout({ quoteFn, input }) { + // input: { chain, tokenIn, tokenOut, amountIn, slippageBps } + console.log(banner("🤖 Agent 1: SCOUT (Planner)")); + console.log("Scout: menerima brief swap…"); + await sleep(250); + + console.log(`Scout: chain=${input.chain}`); + console.log(`Scout: tokenIn=${input.tokenIn}`); + console.log(`Scout: tokenOut=${input.tokenOut}`); + console.log(`Scout: amountIn=${input.amountIn}`); + console.log(`Scout: slippage=${input.slippageBps} bps`); + await sleep(250); + + console.log("Scout: fetching quote…"); + const q = await quoteFn(input); // expect { amountOut, minOut, path, warnings? } + await sleep(200); + + console.log("Scout: quote received ✅"); + console.log(`Scout: estOut = ${fmt(q.amountOut)}`); + console.log(`Scout: minOut = ${fmt(q.minOut)}`); + console.log(`Scout: path = ${q.path?.join(" -> ") || "-"}`); + + // Risk gate (basic + keliatan pro) + const warnings = []; + if (input.slippageBps > 300) warnings.push("Slippage > 3% (risky)"); + if (!q.path || q.path.length < 2) warnings.push("Route path invalid"); + if (q.warnings?.length) warnings.push(...q.warnings); + + console.log("\nScout: risk gate report:"); + if (!warnings.length) { + console.log("Scout: ✅ PASS (no warnings)"); + } else { + console.log("Scout: ⚠️ WARNINGS:"); + for (const w of warnings) console.log(`- ${w}`); + } + + // Plan object buat handoff ke Executor + const plan = { + chain: input.chain, + tokenIn: input.tokenIn, + tokenOut: input.tokenOut, + amountIn: input.amountIn, + slippageBps: input.slippageBps, + amountOut: q.amountOut, + minOut: q.minOut, + path: q.path, + warnings, + }; + + console.log("\nScout: handoff plan → Executor ✅"); + return plan; +} From 43d72930694b72427493d4ed3b657014c55e3a1f Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Tue, 17 Feb 2026 22:10:22 +0700 Subject: [PATCH 03/70] Create agentExecutor.js --- src/agents/agentExecutor.js | 71 +++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/agents/agentExecutor.js diff --git a/src/agents/agentExecutor.js b/src/agents/agentExecutor.js new file mode 100644 index 0000000..06f0112 --- /dev/null +++ b/src/agents/agentExecutor.js @@ -0,0 +1,71 @@ +import { banner, sleep, fmt } from "./agentShared.js"; + +function yn(input) { + const s = String(input || "").trim().toLowerCase(); + return s === "y" || s === "yes"; +} + +export async function agentExecutor({ executeFn, plan, confirmFn }) { + console.log(banner("🤖 Agent 2: EXECUTOR (Strict)")); + console.log("Executor: menerima plan dari Scout…"); + await sleep(250); + + console.log("\nExecutor: REVIEW PLAN"); + console.log(`- chain : ${plan.chain}`); + console.log(`- tokenIn : ${plan.tokenIn}`); + console.log(`- tokenOut : ${plan.tokenOut}`); + console.log(`- amountIn : ${plan.amountIn}`); + console.log(`- slippage : ${plan.slippageBps} bps`); + console.log(`- estOut : ${fmt(plan.amountOut)}`); + console.log(`- minOut : ${fmt(plan.minOut)}`); + console.log(`- path : ${plan.path?.join(" -> ") || "-"}`); + + if (plan.warnings?.length) { + console.log("\nExecutor: ⚠️ WARNINGS from Scout:"); + for (const w of plan.warnings) console.log(`- ${w}`); + } else { + console.log("\nExecutor: no warnings ✅"); + } + + // Confirm gate: kalau ada warnings, Executor jadi “tegas” + const mustConfirm = (plan.warnings?.length || 0) > 0; + + console.log("\nExecutor: confirm gate"); + if (!confirmFn) { + // fallback default: auto-deny if warnings, auto-approve if clean + if (mustConfirm) { + console.log("Executor: confirmFn missing → AUTO-ABORT (warnings present)."); + return { ok: false, reason: "missing_confirmFn_with_warnings" }; + } + console.log("Executor: confirmFn missing → AUTO-APPROVE (no warnings)."); + } else { + const question = mustConfirm + ? "Warnings detected. Proceed anyway? (y/n)" + : "Proceed with swap execution? (y/n)"; + + const answer = await confirmFn(question); + if (!yn(answer)) { + console.log("Executor: user rejected ❌ aborting."); + return { ok: false, reason: "user_rejected" }; + } + console.log("Executor: user confirmed ✅"); + } + + console.log("\nExecutor: executing swap…"); + await sleep(250); + + // executeFn expect returns { txid, status, details? } + const res = await executeFn(plan); + await sleep(200); + + console.log("Executor: execution result ✅"); + console.log(`- status: ${res.status || "unknown"}`); + console.log(`- txid : ${res.txid || "-"}`); + + if (res.details) { + console.log("- details:"); + console.log(res.details); + } + + return { ok: true, ...res }; +} From 9fcf7a8c199f79948864e3d758372f5d39a4ba1d Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Tue, 17 Feb 2026 22:26:17 +0700 Subject: [PATCH 04/70] Update index.js --- index.js | 862 +++++++++++-------------------------------------------- 1 file changed, 173 insertions(+), 689 deletions(-) diff --git a/index.js b/index.js index ade024c..f684c9b 100644 --- a/index.js +++ b/index.js @@ -1,721 +1,205 @@ -/** @typedef {import('pear-interface')} */ -import fs from 'fs'; -import path from 'path'; -import b4a from 'b4a'; -import PeerWallet from 'trac-wallet'; -import { Peer, Wallet, createConfig as createPeerConfig, ENV as PEER_ENV } from 'trac-peer'; -import { MainSettlementBus } from 'trac-msb/src/index.js'; -import { createConfig as createMsbConfig, ENV as MSB_ENV } from 'trac-msb/src/config/env.js'; -import { ensureTextCodecs } from 'trac-peer/src/textCodec.js'; -import { getPearRuntime, ensureTrailingSlash } from 'trac-peer/src/runnerArgs.js'; -import { Terminal } from 'trac-peer/src/terminal/index.js'; -import SampleProtocol from './contract/protocol.js'; -import SampleContract from './contract/contract.js'; -import { Timer } from './features/timer/index.js'; -import Sidechannel from './features/sidechannel/index.js'; -import ScBridge from './features/sc-bridge/index.js'; -import PriceOracleFeature from './features/price/index.js'; - -const { env, storeLabel, flags } = getPearRuntime(); - -const peerStoreNameRaw = - (flags['peer-store-name'] && String(flags['peer-store-name'])) || - env.PEER_STORE_NAME || - storeLabel || - 'peer'; - -const peerStoresDirectory = ensureTrailingSlash( - (flags['peer-stores-directory'] && String(flags['peer-stores-directory'])) || - env.PEER_STORES_DIRECTORY || - 'stores/' -); - -const msbStoreName = - (flags['msb-store-name'] && String(flags['msb-store-name'])) || - env.MSB_STORE_NAME || - `${peerStoreNameRaw}-msb`; - -const msbStoresDirectory = ensureTrailingSlash( - (flags['msb-stores-directory'] && String(flags['msb-stores-directory'])) || - env.MSB_STORES_DIRECTORY || - 'stores/' -); - -const subnetChannel = - (flags['subnet-channel'] && String(flags['subnet-channel'])) || - env.SUBNET_CHANNEL || - 'trac-peer-subnet'; - -const sidechannelsRaw = - (flags['sidechannels'] && String(flags['sidechannels'])) || - (flags['sidechannel'] && String(flags['sidechannel'])) || - env.SIDECHANNELS || - ''; - -const parseBool = (value, fallback) => { - if (value === undefined || value === null || value === '') return fallback; - return ['1', 'true', 'yes', 'on'].includes(String(value).trim().toLowerCase()); -}; - -const parseIntOpt = (value, fallback) => { - if (value === undefined || value === null || value === '') return fallback; - const n = Number.parseInt(String(value), 10); - return Number.isFinite(n) ? n : fallback; -}; - -const parseFloatOpt = (value, fallback) => { - if (value === undefined || value === null || value === '') return fallback; - const n = Number.parseFloat(String(value)); - return Number.isFinite(n) ? n : fallback; -}; - -const msbEnabledRaw = - (flags['msb'] && String(flags['msb'])) || - (flags['enable-msb'] && String(flags['enable-msb'])) || - env.MSB_ENABLED || - ''; -const msbEnabled = parseBool(msbEnabledRaw, true); - -// Terminal UI (interactive) is enabled by default. For daemon/supervisor mode (peermgr/promptd), -// disable it so the process does not exit when stdin is closed. -const terminalEnabledRaw = - (flags['terminal'] && String(flags['terminal'])) || - (flags['enable-terminal'] && String(flags['enable-terminal'])) || - env.TERMINAL_ENABLED || - env.TERMINAL || - ''; -const terminalEnabled = parseBool(terminalEnabledRaw, true); - -const parseKeyValueList = (raw) => { - if (!raw) return []; - return String(raw) - .split(',') - .map((entry) => String(entry || '').trim()) - .filter((entry) => entry.length > 0) - .map((entry) => { - const idx = entry.indexOf(':'); - const alt = entry.indexOf('='); - const splitAt = idx >= 0 ? idx : alt; - if (splitAt <= 0) return null; - const key = entry.slice(0, splitAt).trim(); - const value = entry.slice(splitAt + 1).trim(); - if (!key || !value) return null; - return [key, value]; - }) - .filter(Boolean); -}; - -const parseCsvList = (raw) => { - if (!raw) return null; - return String(raw) - .split(',') - .map((value) => value.trim()) - .filter((value) => value.length > 0); -}; - -const parseWelcomeValue = (raw) => { - if (!raw) return null; - let text = String(raw || '').trim(); - if (!text) return null; - if (text.startsWith('@')) { - try { - const filePath = path.resolve(text.slice(1)); - text = String(fs.readFileSync(filePath, 'utf8') || '').trim(); - if (!text) return null; - } catch (_e) { - return null; - } - } - if (text.startsWith('b64:')) text = text.slice(4); - if (text.startsWith('{')) { - try { - return JSON.parse(text); - } catch (_e) { - return null; - } - } - try { - const decoded = b4a.toString(b4a.from(text, 'base64')); - return JSON.parse(decoded); - } catch (_e) {} - return null; -}; - -const sidechannelDebugRaw = - (flags['sidechannel-debug'] && String(flags['sidechannel-debug'])) || - env.SIDECHANNEL_DEBUG || - ''; -const sidechannelDebug = parseBool(sidechannelDebugRaw, false); -const sidechannelQuietRaw = - (flags['sidechannel-quiet'] && String(flags['sidechannel-quiet'])) || - env.SIDECHANNEL_QUIET || - ''; -const sidechannelQuiet = parseBool(sidechannelQuietRaw, false); -const sidechannelMaxBytesRaw = - (flags['sidechannel-max-bytes'] && String(flags['sidechannel-max-bytes'])) || - env.SIDECHANNEL_MAX_BYTES || - ''; -const sidechannelMaxBytes = Number.parseInt(sidechannelMaxBytesRaw, 10); -const sidechannelAllowRemoteOpenRaw = - (flags['sidechannel-allow-remote-open'] && String(flags['sidechannel-allow-remote-open'])) || - env.SIDECHANNEL_ALLOW_REMOTE_OPEN || - ''; -const sidechannelAllowRemoteOpen = parseBool(sidechannelAllowRemoteOpenRaw, true); -const sidechannelAutoJoinRaw = - (flags['sidechannel-auto-join'] && String(flags['sidechannel-auto-join'])) || - env.SIDECHANNEL_AUTO_JOIN || - ''; -const sidechannelAutoJoin = parseBool(sidechannelAutoJoinRaw, false); -const sidechannelPowRaw = - (flags['sidechannel-pow'] && String(flags['sidechannel-pow'])) || - env.SIDECHANNEL_POW || - ''; -const sidechannelPowEnabled = parseBool(sidechannelPowRaw, true); -const sidechannelPowDifficultyRaw = - (flags['sidechannel-pow-difficulty'] && String(flags['sidechannel-pow-difficulty'])) || - env.SIDECHANNEL_POW_DIFFICULTY || - '12'; -const sidechannelPowDifficulty = Number.parseInt(sidechannelPowDifficultyRaw, 10); -const sidechannelPowEntryRaw = - (flags['sidechannel-pow-entry'] && String(flags['sidechannel-pow-entry'])) || - env.SIDECHANNEL_POW_ENTRY || - ''; -const sidechannelPowRequireEntry = parseBool(sidechannelPowEntryRaw, false); -const sidechannelPowChannelsRaw = - (flags['sidechannel-pow-channels'] && String(flags['sidechannel-pow-channels'])) || - env.SIDECHANNEL_POW_CHANNELS || - ''; -const sidechannelPowChannels = sidechannelPowChannelsRaw - ? sidechannelPowChannelsRaw - .split(',') - .map((value) => value.trim()) - .filter((value) => value.length > 0) - : null; -const sidechannelInviteRequiredRaw = - (flags['sidechannel-invite-required'] && String(flags['sidechannel-invite-required'])) || - env.SIDECHANNEL_INVITE_REQUIRED || - ''; -const sidechannelInviteRequired = parseBool(sidechannelInviteRequiredRaw, false); -const sidechannelInviteChannelsRaw = - (flags['sidechannel-invite-channels'] && String(flags['sidechannel-invite-channels'])) || - env.SIDECHANNEL_INVITE_CHANNELS || - ''; -const sidechannelInviteChannels = sidechannelInviteChannelsRaw - ? sidechannelInviteChannelsRaw - .split(',') - .map((value) => value.trim()) - .filter((value) => value.length > 0) - : null; -const sidechannelInvitePrefixesRaw = - (flags['sidechannel-invite-prefixes'] && String(flags['sidechannel-invite-prefixes'])) || - env.SIDECHANNEL_INVITE_PREFIXES || - ''; -const sidechannelInvitePrefixes = sidechannelInvitePrefixesRaw - ? sidechannelInvitePrefixesRaw - .split(',') - .map((value) => value.trim()) - .filter((value) => value.length > 0) - : null; -const sidechannelInviterKeysRaw = - (flags['sidechannel-inviter-keys'] && String(flags['sidechannel-inviter-keys'])) || - env.SIDECHANNEL_INVITER_KEYS || - ''; -const sidechannelInviterKeys = sidechannelInviterKeysRaw - ? sidechannelInviterKeysRaw - .split(',') - .map((value) => value.trim()) - .filter((value) => value.length > 0) - : []; -const sidechannelInviteTtlRaw = - (flags['sidechannel-invite-ttl'] && String(flags['sidechannel-invite-ttl'])) || - env.SIDECHANNEL_INVITE_TTL || - '604800'; -const sidechannelInviteTtlSec = Number.parseInt(sidechannelInviteTtlRaw, 10); -const sidechannelInviteTtlMs = Number.isFinite(sidechannelInviteTtlSec) - ? Math.max(sidechannelInviteTtlSec, 0) * 1000 - : 0; -const sidechannelOwnerRaw = - (flags['sidechannel-owner'] && String(flags['sidechannel-owner'])) || - env.SIDECHANNEL_OWNER || - ''; -const sidechannelOwnerEntries = parseKeyValueList(sidechannelOwnerRaw); -const sidechannelOwnerMap = new Map(); -for (const [channel, key] of sidechannelOwnerEntries) { - const normalizedKey = key.trim().toLowerCase(); - if (channel && normalizedKey) sidechannelOwnerMap.set(channel.trim(), normalizedKey); -} +import readline from "readline"; -const sidechannelDefaultOwnerRaw = - (flags['sidechannel-default-owner'] && String(flags['sidechannel-default-owner'])) || - env.SIDECHANNEL_DEFAULT_OWNER || - ''; -const sidechannelDefaultOwner = sidechannelDefaultOwnerRaw - ? String(sidechannelDefaultOwnerRaw).trim().toLowerCase() - : null; -if (sidechannelDefaultOwner && !/^[0-9a-f]{64}$/.test(sidechannelDefaultOwner)) { - throw new Error('Invalid --sidechannel-default-owner. Provide 32-byte hex (64 chars).'); +// ===== SHARED UTILS ===== +function banner(title) { + return "\n==============================\n" + title + "\n=============================="; } -const sidechannelOwnerWriteOnlyRaw = - (flags['sidechannel-owner-write-only'] && String(flags['sidechannel-owner-write-only'])) || - env.SIDECHANNEL_OWNER_WRITE_ONLY || - ''; -const sidechannelOwnerWriteOnly = parseBool(sidechannelOwnerWriteOnlyRaw, false); -const sidechannelOwnerWriteChannelsRaw = - (flags['sidechannel-owner-write-channels'] && String(flags['sidechannel-owner-write-channels'])) || - env.SIDECHANNEL_OWNER_WRITE_CHANNELS || - ''; -const sidechannelOwnerWriteChannels = sidechannelOwnerWriteChannelsRaw - ? sidechannelOwnerWriteChannelsRaw - .split(',') - .map((value) => value.trim()) - .filter((value) => value.length > 0) - : null; -const sidechannelWelcomeRaw = - (flags['sidechannel-welcome'] && String(flags['sidechannel-welcome'])) || - env.SIDECHANNEL_WELCOME || - ''; -const sidechannelWelcomeEntries = parseKeyValueList(sidechannelWelcomeRaw); -const sidechannelWelcomeMap = new Map(); -for (const [channel, value] of sidechannelWelcomeEntries) { - const welcome = parseWelcomeValue(value); - if (channel && welcome) sidechannelWelcomeMap.set(channel.trim(), welcome); +function sleep(ms) { + return new Promise((r) => setTimeout(r, ms)); } -const sidechannelWelcomeRequiredRaw = - (flags['sidechannel-welcome-required'] && String(flags['sidechannel-welcome-required'])) || - env.SIDECHANNEL_WELCOME_REQUIRED || - ''; -const sidechannelWelcomeRequired = parseBool(sidechannelWelcomeRequiredRaw, true); - -const sidechannelEntry = '0000intercom'; -const sidechannelExtras = sidechannelsRaw - .split(',') - .map((value) => value.trim()) - .filter((value) => value.length > 0 && value !== sidechannelEntry); - -if (sidechannelWelcomeRequired && !sidechannelOwnerMap.has(sidechannelEntry)) { - console.warn( - `[sidechannel] welcome required for non-entry channels; entry "${sidechannelEntry}" is open and does not require owner/welcome.` - ); + +function fmt(n) { + return Number(n).toFixed(4); } -const subnetBootstrapHex = - (flags['subnet-bootstrap'] && String(flags['subnet-bootstrap'])) || - env.SUBNET_BOOTSTRAP || - null; - -const scBridgeEnabledRaw = - (flags['sc-bridge'] && String(flags['sc-bridge'])) || - env.SC_BRIDGE || - ''; -const scBridgeEnabled = parseBool(scBridgeEnabledRaw, false); -const scBridgeHost = - (flags['sc-bridge-host'] && String(flags['sc-bridge-host'])) || - env.SC_BRIDGE_HOST || - '127.0.0.1'; -const scBridgePortRaw = - (flags['sc-bridge-port'] && String(flags['sc-bridge-port'])) || - env.SC_BRIDGE_PORT || - ''; -const scBridgePort = Number.parseInt(scBridgePortRaw, 10); -const scBridgeFilter = - (flags['sc-bridge-filter'] && String(flags['sc-bridge-filter'])) || - env.SC_BRIDGE_FILTER || - ''; -const scBridgeFilterChannelRaw = - (flags['sc-bridge-filter-channel'] && String(flags['sc-bridge-filter-channel'])) || - env.SC_BRIDGE_FILTER_CHANNEL || - ''; -const scBridgeFilterChannels = scBridgeFilterChannelRaw - ? scBridgeFilterChannelRaw - .split(',') - .map((value) => value.trim()) - .filter((value) => value.length > 0) - : null; -const scBridgeToken = - (flags['sc-bridge-token'] && String(flags['sc-bridge-token'])) || - env.SC_BRIDGE_TOKEN || - ''; -const scBridgeCliRaw = - (flags['sc-bridge-cli'] && String(flags['sc-bridge-cli'])) || - env.SC_BRIDGE_CLI || - ''; -const scBridgeCliEnabled = parseBool(scBridgeCliRaw, false); -const scBridgeDebugRaw = - (flags['sc-bridge-debug'] && String(flags['sc-bridge-debug'])) || - env.SC_BRIDGE_DEBUG || - ''; -const scBridgeDebug = parseBool(scBridgeDebugRaw, false); - -// Optional: override DHT bootstrap nodes (host:port list) for faster local tests. -// Note: this affects all Hyperswarm joins (subnet replication + sidechannels). -const peerDhtBootstrapRaw = - (flags['peer-dht-bootstrap'] && String(flags['peer-dht-bootstrap'])) || - (flags['dht-bootstrap'] && String(flags['dht-bootstrap'])) || - env.PEER_DHT_BOOTSTRAP || - env.DHT_BOOTSTRAP || - ''; -const peerDhtBootstrap = parseCsvList(peerDhtBootstrapRaw); -const msbDhtBootstrapRaw = - (flags['msb-dht-bootstrap'] && String(flags['msb-dht-bootstrap'])) || - env.MSB_DHT_BOOTSTRAP || - ''; -const msbDhtBootstrap = parseCsvList(msbDhtBootstrapRaw); - -const validateDhtBootstrapList = (label, list) => { - if (!list) return; - if (list.length === 0) throw new Error(`Invalid ${label} bootstrap list (empty).`); - for (const entry of list) { - // hyperdht supports [suggested-ip@]:; we validate the port only. - const idx = entry.lastIndexOf(':'); - const port = idx >= 0 ? Number.parseInt(entry.slice(idx + 1), 10) : NaN; - if (idx <= 0 || !Number.isFinite(port) || port <= 0) { - throw new Error(`Invalid ${label} entry: "${entry}" (expected host:port).`); - } +// ===== AGENT SCOUT ===== +async function agentScout({ quoteFn, input }) { + console.log(banner("🤖 Agent 1: SCOUT (Planner)")); + console.log("Scout: menerima brief swap…"); + await sleep(250); + + console.log(`Scout: chain=${input.chain}`); + console.log(`Scout: tokenIn=${input.tokenIn}`); + console.log(`Scout: tokenOut=${input.tokenOut}`); + console.log(`Scout: amountIn=${input.amountIn}`); + console.log(`Scout: slippage=${input.slippageBps} bps`); + await sleep(250); + + console.log("Scout: fetching quote…"); + const q = await quoteFn(input); + await sleep(200); + + console.log("Scout: quote received ✅"); + console.log(`Scout: estOut = ${fmt(q.amountOut)}`); + console.log(`Scout: minOut = ${fmt(q.minOut)}`); + console.log(`Scout: path = ${q.path?.join(" -> ") || "-"}`); + + const warnings = []; + if (input.slippageBps > 300) warnings.push("Slippage > 3% (risky)"); + if (!q.path || q.path.length < 2) warnings.push("Route path invalid"); + if (q.warnings?.length) warnings.push(...q.warnings); + + console.log("\nScout: risk gate report:"); + if (!warnings.length) { + console.log("Scout: ✅ PASS (no warnings)"); + } else { + console.log("Scout: ⚠️ WARNINGS:"); + for (const w of warnings) console.log(`- ${w}`); } -}; -validateDhtBootstrapList('--peer-dht-bootstrap/--dht-bootstrap', peerDhtBootstrap); -validateDhtBootstrapList('--msb-dht-bootstrap', msbDhtBootstrap); - -const priceOracleEnabledRaw = - (flags['price-oracle'] && String(flags['price-oracle'])) || - env.PRICE_ORACLE || - ''; -const priceOracleEnabled = parseBool(priceOracleEnabledRaw, false); -const priceOracleDebugRaw = - (flags['price-oracle-debug'] && String(flags['price-oracle-debug'])) || - env.PRICE_ORACLE_DEBUG || - ''; -const priceOracleDebug = parseBool(priceOracleDebugRaw, false); -const priceOraclePollMsRaw = - (flags['price-poll-ms'] && String(flags['price-poll-ms'])) || - env.PRICE_POLL_MS || - ''; -const priceOraclePollMs = parseIntOpt(priceOraclePollMsRaw, 5000); -const priceOracleProvidersRaw = - (flags['price-providers'] && String(flags['price-providers'])) || - env.PRICE_PROVIDERS || - ''; -const priceOraclePairsRaw = - (flags['price-pairs'] && String(flags['price-pairs'])) || - env.PRICE_PAIRS || - ''; -const priceOraclePairs = priceOraclePairsRaw - ? priceOraclePairsRaw - .split(',') - .map((v) => v.trim()) - .filter((v) => v.length > 0) - : null; -const priceOracleRequiredProvidersRaw = - (flags['price-required-providers'] && String(flags['price-required-providers'])) || - env.PRICE_REQUIRED_PROVIDERS || - ''; -const priceOracleRequiredProviders = parseIntOpt(priceOracleRequiredProvidersRaw, 5); -const priceOracleMinOkRaw = - (flags['price-min-ok'] && String(flags['price-min-ok'])) || - env.PRICE_MIN_OK || - ''; -const priceOracleMinOk = parseIntOpt(priceOracleMinOkRaw, 2); -const priceOracleMinAgreeRaw = - (flags['price-min-agree'] && String(flags['price-min-agree'])) || - env.PRICE_MIN_AGREE || - ''; -const priceOracleMinAgree = parseIntOpt(priceOracleMinAgreeRaw, 2); -const priceOracleMaxDeviationBpsRaw = - (flags['price-max-deviation-bps'] && String(flags['price-max-deviation-bps'])) || - env.PRICE_MAX_DEVIATION_BPS || - ''; -const priceOracleMaxDeviationBps = parseFloatOpt(priceOracleMaxDeviationBpsRaw, 50); -const priceOracleTimeoutMsRaw = - (flags['price-timeout-ms'] && String(flags['price-timeout-ms'])) || - env.PRICE_TIMEOUT_MS || - ''; -const priceOracleTimeoutMs = parseIntOpt(priceOracleTimeoutMsRaw, 4000); -const priceOracleStaticBtcUsdtRaw = - (flags['price-static-btc-usdt'] && String(flags['price-static-btc-usdt'])) || - env.PRICE_STATIC_BTC_USDT || - ''; -const priceOracleStaticBtcUsdt = parseFloatOpt(priceOracleStaticBtcUsdtRaw, null); -const priceOracleStaticUsdtUsdRaw = - (flags['price-static-usdt-usd'] && String(flags['price-static-usdt-usd'])) || - env.PRICE_STATIC_USDT_USD || - ''; -const priceOracleStaticUsdtUsd = parseFloatOpt(priceOracleStaticUsdtUsdRaw, null); -const priceOracleStaticCountRaw = - (flags['price-static-count'] && String(flags['price-static-count'])) || - env.PRICE_STATIC_COUNT || - ''; -const priceOracleStaticCount = parseIntOpt(priceOracleStaticCountRaw, 5); - -if (scBridgeEnabled && !scBridgeToken) { - throw new Error('SC-Bridge requires --sc-bridge-token (auth is mandatory).'); + + const plan = { + ...input, + amountOut: q.amountOut, + minOut: q.minOut, + path: q.path, + warnings, + }; + + console.log("\nScout: handoff plan → Executor ✅"); + return plan; } -const readHexFile = (filePath, byteLength) => { - try { - if (fs.existsSync(filePath)) { - const hex = fs.readFileSync(filePath, 'utf8').trim().toLowerCase(); - if (/^[0-9a-f]+$/.test(hex) && hex.length === byteLength * 2) return hex; - } - } catch (_e) {} - return null; -}; - -const subnetBootstrapFile = path.join( - peerStoresDirectory, - peerStoreNameRaw, - 'subnet-bootstrap.hex' -); - -let subnetBootstrap = subnetBootstrapHex ? subnetBootstrapHex.trim().toLowerCase() : null; -if (subnetBootstrap) { - if (!/^[0-9a-f]{64}$/.test(subnetBootstrap)) { - throw new Error('Invalid --subnet-bootstrap. Provide 32-byte hex (64 chars).'); - } -} else { - subnetBootstrap = readHexFile(subnetBootstrapFile, 32); +// ===== AGENT EXECUTOR ===== +function yn(input) { + const s = String(input || "").trim().toLowerCase(); + return s === "y" || s === "yes"; } -const msbConfig = createMsbConfig(MSB_ENV.MAINNET, { - storeName: msbStoreName, - storesDirectory: msbStoresDirectory, - enableInteractiveMode: false, - ...(Array.isArray(msbDhtBootstrap) ? { dhtBootstrap: msbDhtBootstrap } : {}), -}); +async function agentExecutor({ executeFn, plan, confirmFn }) { + console.log(banner("🤖 Agent 2: EXECUTOR (Strict)")); + console.log("Executor: menerima plan dari Scout…"); + await sleep(250); -const msbBootstrapHex = b4a.toString(msbConfig.bootstrap, 'hex'); -if (subnetBootstrap && subnetBootstrap === msbBootstrapHex) { - throw new Error('Subnet bootstrap cannot equal MSB bootstrap.'); -} + console.log("\nExecutor: REVIEW PLAN"); + console.log(`- chain : ${plan.chain}`); + console.log(`- tokenIn : ${plan.tokenIn}`); + console.log(`- tokenOut : ${plan.tokenOut}`); + console.log(`- amountIn : ${plan.amountIn}`); + console.log(`- slippage : ${plan.slippageBps} bps`); + console.log(`- estOut : ${fmt(plan.amountOut)}`); + console.log(`- minOut : ${fmt(plan.minOut)}`); + console.log(`- path : ${plan.path?.join(" -> ") || "-"}`); -const peerConfig = createPeerConfig(PEER_ENV.MAINNET, { - storesDirectory: peerStoresDirectory, - storeName: peerStoreNameRaw, - bootstrap: subnetBootstrap || null, - channel: subnetChannel, - enableInteractiveMode: true, - enableBackgroundTasks: true, - enableUpdater: true, - replicate: true, - ...(Array.isArray(peerDhtBootstrap) ? { dhtBootstrap: peerDhtBootstrap } : {}), -}); - -const ensureKeypairFile = async (keyPairPath) => { - if (fs.existsSync(keyPairPath)) return; - fs.mkdirSync(path.dirname(keyPairPath), { recursive: true }); - await ensureTextCodecs(); - const wallet = new PeerWallet(); - await wallet.ready; - if (!wallet.secretKey) { - await wallet.generateKeyPair(); + if (plan.warnings?.length) { + console.log("\nExecutor: ⚠️ WARNINGS:"); + for (const w of plan.warnings) console.log(`- ${w}`); + } else { + console.log("\nExecutor: no warnings ✅"); + } + + const mustConfirm = (plan.warnings?.length || 0) > 0; + + console.log("\nExecutor: confirm gate"); + if (!confirmFn) { + if (mustConfirm) { + console.log("Executor: AUTO-ABORT (warnings present)"); + return { ok: false }; + } + console.log("Executor: AUTO-APPROVE (no warnings)"); + } else { + const q = mustConfirm + ? "Warnings detected. Proceed anyway? (y/n)" + : "Proceed with swap? (y/n)"; + + const ans = await confirmFn(q); + if (!yn(ans)) { + console.log("Executor: rejected ❌"); + return { ok: false }; + } + console.log("Executor: confirmed ✅"); } - wallet.exportToFile(keyPairPath, b4a.alloc(0)); -}; -if (msbEnabled) { - await ensureKeypairFile(msbConfig.keyPairPath); + console.log("\nExecutor: executing swap…"); + await sleep(300); + + const res = await executeFn(plan); + + console.log("Executor: execution result ✅"); + console.log(`- status: ${res.status}`); + console.log(`- txid : ${res.txid}`); + + return { ok: true, ...res }; } -await ensureKeypairFile(peerConfig.keyPairPath); - -let msb = null; -if (msbEnabled) { - console.log('=============== STARTING MSB ==============='); - msb = new MainSettlementBus(msbConfig); - await msb.ready(); -} else { - console.log('=============== MSB DISABLED ==============='); - // Provide a minimal MSB surface so trac-peer can initialize MsbClient without networking. - msb = { - config: msbConfig, - state: { - getIndexerSequenceState: async () => b4a.alloc(0), - getSignedLength: () => 0, - getUnsignedLength: () => 0, - getFee: () => null, - getNodeEntryUnsigned: async () => null, - }, - network: { - validatorConnectionManager: { connectionCount: () => 0 }, - }, - ready: async () => {}, - broadcastTransactionCommand: async () => ({ message: 'MSB disabled.', tx: null }), + +// ===== MOCK BACKEND ===== +async function quoteFn(input) { + return { + amountOut: Number(input.amountIn) * 0.95, + minOut: Number(input.amountIn) * 0.92, + path: [input.tokenIn, "USDC", input.tokenOut], + warnings: input.tokenOut === "SCAM" ? ["Token risky"] : [], }; } -console.log('=============== STARTING PEER ==============='); -const peer = new Peer({ - config: peerConfig, - msb, - wallet: new Wallet(), - protocol: SampleProtocol, - contract: SampleContract, -}); -await peer.ready(); - -const effectiveSubnetBootstrapHex = peer.base?.key - ? peer.base.key.toString('hex') - : b4a.isBuffer(peer.config.bootstrap) - ? peer.config.bootstrap.toString('hex') - : String(peer.config.bootstrap ?? '').toLowerCase(); - -if (!subnetBootstrap) { - fs.mkdirSync(path.dirname(subnetBootstrapFile), { recursive: true }); - fs.writeFileSync(subnetBootstrapFile, `${effectiveSubnetBootstrapHex}\n`); +async function executeFn(plan) { + return { + status: "success", + txid: "0x" + Math.random().toString(16).slice(2), + }; } -console.log(''); -console.log('====================INTERCOM ===================='); -const msbChannel = b4a.toString(msbConfig.channel, 'utf8'); -const msbStorePath = path.join(msbStoresDirectory, msbStoreName); -const peerStorePath = path.join(peerStoresDirectory, peerStoreNameRaw); -const peerWriterKey = peer.writerLocalKey ?? peer.base?.local?.key?.toString('hex') ?? null; -console.log('MSB network bootstrap:', msbBootstrapHex); -console.log('MSB channel:', msbChannel); -console.log('MSB store:', msbStorePath); -console.log('Peer store:', peerStorePath); -if (Array.isArray(msbConfig?.dhtBootstrap) && msbConfig.dhtBootstrap.length > 0) { - console.log('MSB DHT bootstrap nodes:', msbConfig.dhtBootstrap.join(', ')); -} -if (Array.isArray(peerConfig?.dhtBootstrap) && peerConfig.dhtBootstrap.length > 0) { - console.log('Peer DHT bootstrap nodes:', peerConfig.dhtBootstrap.join(', ')); -} -console.log('Peer subnet bootstrap:', effectiveSubnetBootstrapHex); -console.log('Peer subnet channel:', subnetChannel); -console.log('Peer pubkey (hex):', peer.wallet.publicKey); -console.log('Peer trac address (bech32m):', peer.wallet.address ?? null); -console.log('Peer writer key (hex):', peerWriterKey); -console.log('Sidechannel entry:', sidechannelEntry); -if (sidechannelExtras.length > 0) { - console.log('Sidechannel extras:', sidechannelExtras.join(', ')); -} -if (scBridgeEnabled) { - const portDisplay = Number.isSafeInteger(scBridgePort) ? scBridgePort : 49222; - console.log('SC-Bridge:', `ws://${scBridgeHost}:${portDisplay}`); -} -console.log('================================================================'); -console.log(''); - -const admin = await peer.base.view.get('admin'); -if (admin && admin.value === peer.wallet.publicKey && peer.base.writable) { - const timer = new Timer(peer, { update_interval: 60_000 }); - await peer.protocol.instance.addFeature('timer', timer); - timer.start().catch((err) => console.error('Timer feature stopped:', err?.message ?? err)); -} +function confirmFn(q) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); -let priceOracle = null; -if (priceOracleEnabled) { - const staticPrices = {}; - if (Number.isFinite(priceOracleStaticBtcUsdt) && priceOracleStaticBtcUsdt > 0) { - staticPrices.BTC_USDT = priceOracleStaticBtcUsdt; - } - if (Number.isFinite(priceOracleStaticUsdtUsd) && priceOracleStaticUsdtUsd > 0) { - staticPrices.USDT_USD = priceOracleStaticUsdtUsd; - } - priceOracle = new PriceOracleFeature(peer, { - pollMs: priceOraclePollMs, - debug: priceOracleDebug, - oracleOptions: { - ...(priceOracleProvidersRaw ? { providerIds: priceOracleProvidersRaw } : {}), - ...(priceOraclePairs ? { pairs: priceOraclePairs } : {}), - requiredProviders: priceOracleRequiredProviders, - minOk: priceOracleMinOk, - minAgree: priceOracleMinAgree, - maxDeviationBps: priceOracleMaxDeviationBps, - timeoutMs: priceOracleTimeoutMs, - ...(Object.keys(staticPrices).length > 0 ? { staticPrices, staticCount: priceOracleStaticCount } : {}), - }, + return new Promise((res) => { + rl.question(q + " ", (ans) => { + rl.close(); + res(ans); + }); }); - priceOracle.start(); - peer.priceOracle = priceOracle; } -let scBridge = null; -if (scBridgeEnabled) { - scBridge = new ScBridge(peer, { - host: scBridgeHost, - port: Number.isSafeInteger(scBridgePort) ? scBridgePort : 49222, - filter: scBridgeFilter, - filterChannels: scBridgeFilterChannels || undefined, - token: scBridgeToken, - debug: scBridgeDebug, - cliEnabled: scBridgeCliEnabled, - requireAuth: true, - info: { - msbBootstrap: msbBootstrapHex, - msbChannel, - msbStore: msbStorePath, - msbDhtBootstrap: Array.isArray(msbConfig?.dhtBootstrap) ? msbConfig.dhtBootstrap.slice() : null, - peerStore: peerStorePath, - peerDhtBootstrap: Array.isArray(peerConfig?.dhtBootstrap) ? peerConfig.dhtBootstrap.slice() : null, - subnetBootstrap: effectiveSubnetBootstrapHex, - subnetChannel, - peerPubkey: peer.wallet.publicKey, - peerTracAddress: peer.wallet.address ?? null, - peerWriterKey, - sidechannelEntry, - sidechannelExtras: sidechannelExtras.slice(), - priceOracleEnabled, - }, - }); +// ===== MAIN FLOW ===== +async function runSwap() { + const input = { + chain: "solana", + tokenIn: "SOL", + tokenOut: "USDC", + amountIn: 1, + slippageBps: 100, + }; + + console.log("\n🚀 START MULTI-AGENT SWAP"); + + const plan = await agentScout({ quoteFn, input }); + const result = await agentExecutor({ executeFn, plan, confirmFn }); + + console.log("\n=== FINAL RESULT ==="); + console.log(result); } -const sidechannel = new Sidechannel(peer, { - channels: [sidechannelEntry, ...sidechannelExtras], - debug: sidechannelDebug, - maxMessageBytes: Number.isSafeInteger(sidechannelMaxBytes) ? sidechannelMaxBytes : undefined, - entryChannel: sidechannelEntry, - allowRemoteOpen: sidechannelAllowRemoteOpen, - autoJoinOnOpen: sidechannelAutoJoin, - powEnabled: sidechannelPowEnabled, - powDifficulty: Number.isInteger(sidechannelPowDifficulty) ? sidechannelPowDifficulty : undefined, - powRequireEntry: sidechannelPowRequireEntry, - powRequiredChannels: sidechannelPowChannels || undefined, - inviteRequired: sidechannelInviteRequired, - inviteRequiredChannels: sidechannelInviteChannels || undefined, - inviteRequiredPrefixes: sidechannelInvitePrefixes || undefined, - inviterKeys: sidechannelInviterKeys, - inviteTtlMs: sidechannelInviteTtlMs, - welcomeRequired: sidechannelWelcomeRequired, - ownerWriteOnly: sidechannelOwnerWriteOnly, - ownerWriteChannels: sidechannelOwnerWriteChannels || undefined, - ownerKeys: sidechannelOwnerMap.size > 0 ? sidechannelOwnerMap : undefined, - defaultOwnerKey: sidechannelDefaultOwner || undefined, - welcomeByChannel: sidechannelWelcomeMap.size > 0 ? sidechannelWelcomeMap : undefined, - onMessage: scBridgeEnabled - ? (channel, payload, connection) => scBridge.handleSidechannelMessage(channel, payload, connection) - : sidechannelQuiet - ? () => {} - : null, -}); -peer.sidechannel = sidechannel; - -if (scBridge) { - if (priceOracle) scBridge.attachPriceOracle(priceOracle); - scBridge.attachSidechannel(sidechannel); - try { - scBridge.start(); - } catch (err) { - console.error('SC-Bridge failed to start:', err?.message ?? err); - } - peer.scBridge = scBridge; +// ===== MENU ===== +function menu() { + console.log(` +==== MENU ==== +1. Run Swap (Multi-Agent) +2. Exit +`); } -sidechannel - .start() - .then(() => { - console.log('Sidechannel: ready'); - }) - .catch((err) => { - console.error('Sidechannel failed to start:', err?.message ?? err); +function ask() { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, }); -if (terminalEnabled) { - const terminal = new Terminal(peer); - await terminal.start(); -} else { - console.log('Terminal: disabled (--terminal 0)'); - // Keep the process alive even if underlying sockets/timers have been unref'ed by deps/runtime. - // This is required for supervisor/daemon mode (peermgr/promptd). - const keepAlive = setInterval(() => {}, 1 << 30); - if (keepAlive && typeof keepAlive.ref === 'function') keepAlive.ref(); + rl.question("Choose: ", async (ans) => { + rl.close(); + + if (ans === "1") { + await runSwap(); + ask(); + } else { + console.log("Bye 👋"); + process.exit(0); + } + }); } + +// ===== START ===== +menu(); +ask(); From d2e826b0b7ebdb31ff217a25b08f33c4be24ca21 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Tue, 17 Feb 2026 22:45:01 +0700 Subject: [PATCH 05/70] Update package.json --- package.json | 35 +++++++++-------------------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 8d3d464..cc02db9 100644 --- a/package.json +++ b/package.json @@ -1,33 +1,16 @@ { - "name": "contract-test-latest", - "version": "0.0.1", + "name": "trading-agent", + "version": "1.0.0", "type": "module", - "main": "index.js", + "main": "server.js", "scripts": { - "test": "node --test test/*.test.js", - "test:e2e": "node --test --test-concurrency=1 test-e2e/*.test.js" - }, - "pear": { - "name": "contract-test-latest", - "type": "terminal" + "start": "node server.js" }, "dependencies": { - "@solana/spl-token": "^0.4.14", - "@solana/web3.js": "^1.98.4", - "b4a": "^1.6.7", - "bare-ws": "2.0.3", - "bech32": "^2.0.0", - "compact-encoding": "^2.18.0", - "crypto": "npm:bare-node-crypto", - "fs": "npm:bare-node-fs", - "path": "npm:bare-node-path", - "protomux": "^3.10.1", - "trac-msb": "git+https://github.com/Trac-Systems/main_settlement_bus.git#5088921", - "trac-peer": "git+https://github.com/Trac-Systems/trac-peer.git#d108f52", - "trac-wallet": "1.0.1", - "util": "npm:bare-node-util" - }, - "overrides": { - "trac-wallet": "1.0.1" + "express": "^4.18.2", + "body-parser": "^1.20.2", + "node-fetch": "^3.3.2", + "ethers": "^6.7.0", + "@solana/web3.js": "^1.95.3" } } From 2fc39ed7056116cf70f1238535d7113c1d6c171a Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Tue, 17 Feb 2026 22:46:15 +0700 Subject: [PATCH 06/70] Create server.js --- server.js | 127 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 server.js diff --git a/server.js b/server.js new file mode 100644 index 0000000..5d8305d --- /dev/null +++ b/server.js @@ -0,0 +1,127 @@ +import express from "express"; +import bodyParser from "body-parser"; +import fetch from "node-fetch"; +import { ethers } from "ethers"; +import { Connection } from "@solana/web3.js"; + +const app = express(); +app.use(bodyParser.json()); +app.use(express.static("public")); + +const PORT = 3000; + +// ================= CONFIG ================= +const GROQ_API_KEY = "ISI_GROQ_API_KEY"; + +// EVM +const EVM_RPC = "https://rpc.ankr.com/eth"; +const EVM_PRIVATE_KEY = "ISI_PRIVATE_KEY"; +const EVM_ADDRESS = "ADDRESS_EVM"; + +// SOL +const SOL_RPC = "https://api.mainnet-beta.solana.com"; +const SOL_ADDRESS = "ADDRESS_SOL"; + +// INIT +const provider = new ethers.JsonRpcProvider(EVM_RPC); +const wallet = new ethers.Wallet(EVM_PRIVATE_KEY, provider); +const connection = new Connection(SOL_RPC); + +// ================= AI ================= +async function aiDecision(token) { + const res = await fetch("https://api.groq.com/openai/v1/chat/completions", { + method: "POST", + headers: { + "Authorization": `Bearer ${GROQ_API_KEY}`, + "Content-Type": "application/json" + }, + body: JSON.stringify({ + model: "llama3-70b-8192", + messages: [ + { + role: "user", + content: `Analyze ${token}. Answer BUY / SELL / SKIP` + } + ] + }) + }); + + const data = await res.json(); + return data.choices[0].message.content; +} + +// ================= BALANCE ================= +app.get("/balance", async (req, res) => { + try { + const evmBal = await provider.getBalance(EVM_ADDRESS); + const solBal = await connection.getBalance(SOL_ADDRESS); + + res.json({ + evm: ethers.formatEther(evmBal), + sol: solBal / 1e9 + }); + } catch (e) { + res.json({ error: e.message }); + } +}); + +// ================= TRENDING ================= +app.get("/trending", async (req, res) => { + try { + const r = await fetch("https://api.dexscreener.com/latest/dex/tokens/solana"); + const data = await r.json(); + + res.json(data.pairs.slice(0, 5)); + } catch (e) { + res.json({ error: e.message }); + } +}); + +// ================= SWAP EVM ================= +app.post("/swap-evm", async (req, res) => { + try { + const r = await fetch("https://li.quest/v1/quote", { + method: "POST", + headers: {"Content-Type":"application/json"}, + body: JSON.stringify({ + fromChain: 1, + toChain: 1, + fromToken: "USDC", + toToken: "ETH", + fromAmount: "1000000", + fromAddress: EVM_ADDRESS + }) + }); + + const data = await r.json(); + const tx = data.transactionRequest; + + const txResponse = await wallet.sendTransaction({ + to: tx.to, + data: tx.data, + value: tx.value + }); + + res.json({ tx: txResponse.hash }); + + } catch (e) { + res.json({ error: e.message }); + } +}); + +// ================= AUTO TRADE ================= +app.post("/auto-trade", async (req, res) => { + try { + const token = "SOL"; + const decision = await aiDecision(token); + + res.json({ decision }); + } catch (e) { + res.json({ error: e.message }); + } +}); + +// ================= START ================= +app.listen(PORT, () => { + console.log(`🚀 RUNNING: http://localhost:${PORT}`); +}); From b00f3e75ff35cd1cc26be361217249c38bf6d8cc Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Tue, 17 Feb 2026 22:47:35 +0700 Subject: [PATCH 07/70] Create index.html --- public/index.html | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 public/index.html diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..d10f313 --- /dev/null +++ b/public/index.html @@ -0,0 +1,27 @@ + + + + Trading Agent + + + + +
+

🚀 Trading Agent Dashboard

+ + + +
+ + + + + +
+ +
+
+ + + + From 0ab4398e3d10b99c4bc0a632ce0d3573393aa795 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Tue, 17 Feb 2026 22:48:38 +0700 Subject: [PATCH 08/70] Create style.css --- public/style.css | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 public/style.css diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..32e27ab --- /dev/null +++ b/public/style.css @@ -0,0 +1,36 @@ +body { + background: #0d1117; + color: white; + font-family: Arial; + text-align: center; +} + +.container { + margin-top: 100px; +} + +input { + padding: 10px; + width: 250px; + border-radius: 8px; + border: none; +} + +button { + margin: 8px; + padding: 10px 20px; + border-radius: 8px; + border: none; + cursor: pointer; + background: #238636; + color: white; +} + +button:hover { + background: #2ea043; +} + +#output { + margin-top: 20px; + font-size: 18px; +} From a70e88a35f7b997a69a822fa44d27ea2fd0fdf63 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Tue, 17 Feb 2026 22:49:16 +0700 Subject: [PATCH 09/70] Create app.js --- public/app.js | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 public/app.js diff --git a/public/app.js b/public/app.js new file mode 100644 index 0000000..bba6396 --- /dev/null +++ b/public/app.js @@ -0,0 +1,46 @@ +async function runAI() { + const token = document.getElementById("token").value; + + const res = await fetch("/ai", { + method: "POST", + headers: {"Content-Type":"application/json"}, + body: JSON.stringify({ token }) + }); + + const data = await res.json(); + + document.getElementById("output").innerText = + "AI: " + data.decision; +} + +async function swap() { + const res = await fetch("/swap-evm", { method: "POST" }); + const data = await res.json(); + + document.getElementById("output").innerText = + "TX: " + (data.tx || data.error); +} + +async function getBalance() { + const res = await fetch("/balance"); + const data = await res.json(); + + document.getElementById("output").innerText = + `EVM: ${data.evm} ETH | SOL: ${data.sol}`; +} + +async function trending() { + const res = await fetch("/trending"); + const data = await res.json(); + + document.getElementById("output").innerText = + "🔥 " + data[0].baseToken.symbol; +} + +async function autoTrade() { + const res = await fetch("/auto-trade", { method: "POST" }); + const data = await res.json(); + + document.getElementById("output").innerText = + "AI Decision: " + data.decision; +} From e73de2840dff9480464f1f8d8195131d85c33f96 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Tue, 17 Feb 2026 23:05:30 +0700 Subject: [PATCH 10/70] Update server.js --- server.js | 82 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 49 insertions(+), 33 deletions(-) diff --git a/server.js b/server.js index 5d8305d..5f51e24 100644 --- a/server.js +++ b/server.js @@ -1,8 +1,11 @@ import express from "express"; import bodyParser from "body-parser"; import fetch from "node-fetch"; -import { ethers } from "ethers"; +import { ethers, Wallet } from "ethers"; import { Connection } from "@solana/web3.js"; +import dotenv from "dotenv"; + +dotenv.config(); const app = express(); app.use(bodyParser.json()); @@ -11,22 +14,42 @@ app.use(express.static("public")); const PORT = 3000; // ================= CONFIG ================= -const GROQ_API_KEY = "ISI_GROQ_API_KEY"; +const GROQ_API_KEY = process.env.GROQ_API_KEY; -// EVM const EVM_RPC = "https://rpc.ankr.com/eth"; -const EVM_PRIVATE_KEY = "ISI_PRIVATE_KEY"; -const EVM_ADDRESS = "ADDRESS_EVM"; - -// SOL const SOL_RPC = "https://api.mainnet-beta.solana.com"; -const SOL_ADDRESS = "ADDRESS_SOL"; -// INIT const provider = new ethers.JsonRpcProvider(EVM_RPC); -const wallet = new ethers.Wallet(EVM_PRIVATE_KEY, provider); const connection = new Connection(SOL_RPC); +// ================= WALLET ================= +let CURRENT_WALLET = null; + +// generate wallet +app.get("/generate-wallet", (req, res) => { + const wallet = Wallet.createRandom(); + CURRENT_WALLET = wallet; + + res.json({ + address: wallet.address, + privateKey: wallet.privateKey + }); +}); + +// set wallet from PK +app.post("/set-wallet", (req, res) => { + const { privateKey } = req.body; + + try { + const wallet = new Wallet(privateKey, provider); + CURRENT_WALLET = wallet; + + res.json({ address: wallet.address }); + } catch { + res.json({ error: "Invalid private key" }); + } +}); + // ================= AI ================= async function aiDecision(token) { const res = await fetch("https://api.groq.com/openai/v1/chat/completions", { @@ -53,33 +76,28 @@ async function aiDecision(token) { // ================= BALANCE ================= app.get("/balance", async (req, res) => { try { - const evmBal = await provider.getBalance(EVM_ADDRESS); - const solBal = await connection.getBalance(SOL_ADDRESS); + if (!CURRENT_WALLET) { + return res.json({ error: "Set wallet first" }); + } + + const evmBal = await provider.getBalance(CURRENT_WALLET.address); res.json({ - evm: ethers.formatEther(evmBal), - sol: solBal / 1e9 + address: CURRENT_WALLET.address, + evm: ethers.formatEther(evmBal) }); } catch (e) { res.json({ error: e.message }); } }); -// ================= TRENDING ================= -app.get("/trending", async (req, res) => { - try { - const r = await fetch("https://api.dexscreener.com/latest/dex/tokens/solana"); - const data = await r.json(); - - res.json(data.pairs.slice(0, 5)); - } catch (e) { - res.json({ error: e.message }); - } -}); - -// ================= SWAP EVM ================= +// ================= SWAP ================= app.post("/swap-evm", async (req, res) => { try { + if (!CURRENT_WALLET) { + return res.json({ error: "Set wallet first" }); + } + const r = await fetch("https://li.quest/v1/quote", { method: "POST", headers: {"Content-Type":"application/json"}, @@ -89,14 +107,14 @@ app.post("/swap-evm", async (req, res) => { fromToken: "USDC", toToken: "ETH", fromAmount: "1000000", - fromAddress: EVM_ADDRESS + fromAddress: CURRENT_WALLET.address }) }); const data = await r.json(); const tx = data.transactionRequest; - const txResponse = await wallet.sendTransaction({ + const txResponse = await CURRENT_WALLET.sendTransaction({ to: tx.to, data: tx.data, value: tx.value @@ -112,9 +130,7 @@ app.post("/swap-evm", async (req, res) => { // ================= AUTO TRADE ================= app.post("/auto-trade", async (req, res) => { try { - const token = "SOL"; - const decision = await aiDecision(token); - + const decision = await aiDecision("ETH"); res.json({ decision }); } catch (e) { res.json({ error: e.message }); @@ -123,5 +139,5 @@ app.post("/auto-trade", async (req, res) => { // ================= START ================= app.listen(PORT, () => { - console.log(`🚀 RUNNING: http://localhost:${PORT}`); + console.log(`🚀 RUNNING http://localhost:${PORT}`); }); From 9ceda88e24526750ef9a6e778b629a0241ba52c1 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Tue, 17 Feb 2026 23:06:32 +0700 Subject: [PATCH 11/70] Update index.html --- public/index.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/public/index.html b/public/index.html index d10f313..2e633c9 100644 --- a/public/index.html +++ b/public/index.html @@ -7,16 +7,16 @@
-

🚀 Trading Agent Dashboard

+

🚀 Trading Agent

- +
- - + + - - + +
From 6f83f2009725363a337228b6e81626fccf759e1c Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Tue, 17 Feb 2026 23:07:12 +0700 Subject: [PATCH 12/70] Update app.js --- public/app.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/public/app.js b/public/app.js index bba6396..0e87ed8 100644 --- a/public/app.js +++ b/public/app.js @@ -1,24 +1,24 @@ -async function runAI() { - const token = document.getElementById("token").value; +async function setWallet() { + const pk = document.getElementById("pk").value; - const res = await fetch("/ai", { + const res = await fetch("/set-wallet", { method: "POST", headers: {"Content-Type":"application/json"}, - body: JSON.stringify({ token }) + body: JSON.stringify({ privateKey: pk }) }); const data = await res.json(); document.getElementById("output").innerText = - "AI: " + data.decision; + data.address || data.error; } -async function swap() { - const res = await fetch("/swap-evm", { method: "POST" }); +async function generateWallet() { + const res = await fetch("/generate-wallet"); const data = await res.json(); document.getElementById("output").innerText = - "TX: " + (data.tx || data.error); + `Address: ${data.address}\nPK: ${data.privateKey}`; } async function getBalance() { @@ -26,15 +26,15 @@ async function getBalance() { const data = await res.json(); document.getElementById("output").innerText = - `EVM: ${data.evm} ETH | SOL: ${data.sol}`; + data.evm ? `Balance: ${data.evm} ETH` : data.error; } -async function trending() { - const res = await fetch("/trending"); +async function swap() { + const res = await fetch("/swap-evm", { method: "POST" }); const data = await res.json(); document.getElementById("output").innerText = - "🔥 " + data[0].baseToken.symbol; + data.tx || data.error; } async function autoTrade() { @@ -42,5 +42,5 @@ async function autoTrade() { const data = await res.json(); document.getElementById("output").innerText = - "AI Decision: " + data.decision; + data.decision || data.error; } From 9863208e0aea0ba783fe13911473dd4acf7da65b Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Wed, 18 Feb 2026 00:03:03 +0700 Subject: [PATCH 13/70] Update package.json --- package.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index cc02db9..bedaaf1 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,16 @@ { - "name": "trading-agent", - "version": "1.0.0", + "name": "intercom-swap-by-pakeko", + "version": "3.1.0", "type": "module", "main": "server.js", "scripts": { "start": "node server.js" }, "dependencies": { - "express": "^4.18.2", - "body-parser": "^1.20.2", - "node-fetch": "^3.3.2", - "ethers": "^6.7.0", - "@solana/web3.js": "^1.95.3" + "@solana/web3.js": "^1.95.4", + "bs58": "^5.0.0", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2" } } From d9f2c0da12ee9d5e4c31cf9b209e6b65b5c355b8 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Wed, 18 Feb 2026 00:03:53 +0700 Subject: [PATCH 14/70] Create .env.example --- .env.example | 1 + 1 file changed, 1 insertion(+) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ + From 26a77f6e4c1a87a3bd5b5351da9a7f3209f137e7 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Wed, 18 Feb 2026 00:04:48 +0700 Subject: [PATCH 15/70] Update .env.example --- .env.example | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.env.example b/.env.example index 8b13789..3a58e8f 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,9 @@ +PORT=3000 + +# Solana RPC (default mainnet) +SOL_RPC=https://api.mainnet-beta.solana.com + +# Optional security gate (recommended) +# If set, you MUST send header x-api-key: for setup/swap/balance +API_KEY= From dee533d334c525a42b9dc936da937f930fd974c8 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Wed, 18 Feb 2026 00:05:26 +0700 Subject: [PATCH 16/70] Update .gitignore --- .gitignore | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index 65ca4be..837f7f8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,28 +1,3 @@ -.nvmrc -stores node_modules -ui/**/node_modules -ui/**/dist -ui/**/dist-ssr - -# IDE / OS noise -.idea/ +.env .DS_Store - -# Test artifacts -test-results/ - -# Local planning / scratch (intentionally untracked) -progress.md - -# Local chain/node data, configs, secrets (intentionally untracked) -onchain/ - -# Rust/Solana build artifacts (may include generated keypairs) -target/ -**/target/ - -.claude -.codex -training/ -llama.cpp/ From f9e8bfc95249dc1f53b565b13b65d7fc66132ab6 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Wed, 18 Feb 2026 00:06:12 +0700 Subject: [PATCH 17/70] Update server.js --- server.js | 274 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 177 insertions(+), 97 deletions(-) diff --git a/server.js b/server.js index 5f51e24..15bd373 100644 --- a/server.js +++ b/server.js @@ -1,143 +1,223 @@ import express from "express"; -import bodyParser from "body-parser"; -import fetch from "node-fetch"; -import { ethers, Wallet } from "ethers"; -import { Connection } from "@solana/web3.js"; +import cors from "cors"; +import path from "path"; +import { fileURLToPath } from "url"; import dotenv from "dotenv"; +import bs58 from "bs58"; +import { + Connection, + Keypair, + VersionedTransaction, +} from "@solana/web3.js"; dotenv.config(); const app = express(); -app.use(bodyParser.json()); -app.use(express.static("public")); +app.use(cors()); +app.use(express.json({ limit: "3mb" })); -const PORT = 3000; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); -// ================= CONFIG ================= -const GROQ_API_KEY = process.env.GROQ_API_KEY; +const PORT = process.env.PORT || 3000; -const EVM_RPC = "https://rpc.ankr.com/eth"; -const SOL_RPC = "https://api.mainnet-beta.solana.com"; +// ===== SOLANA CONFIG ===== +const SOL_RPC = process.env.SOL_RPC || "https://api.mainnet-beta.solana.com"; +const solConn = new Connection(SOL_RPC, "confirmed"); -const provider = new ethers.JsonRpcProvider(EVM_RPC); -const connection = new Connection(SOL_RPC); +// ===== OPTIONAL API KEY (recommended) ===== +const API_KEY = (process.env.API_KEY || "").trim(); -// ================= WALLET ================= -let CURRENT_WALLET = null; +// ===== IN-MEMORY SOL WALLET (RUNTIME SETUP) ===== +let SOL_KP = null; -// generate wallet -app.get("/generate-wallet", (req, res) => { - const wallet = Wallet.createRandom(); - CURRENT_WALLET = wallet; +// ---------------- Helpers ---------------- +function ok(res, data) { + res.json({ ok: true, ...data }); +} +function fail(res, msg, extra = {}) { + res.status(400).json({ ok: false, error: msg, ...extra }); +} +function mask(addr) { + if (!addr) return ""; + return `${addr.slice(0, 6)}...${addr.slice(-4)}`; +} +function requireApiKey(req, res) { + if (!API_KEY) return true; // unlocked if not set + const got = req.headers["x-api-key"]; + if (got !== API_KEY) { + res + .status(401) + .json({ ok: false, error: "Unauthorized (missing/invalid x-api-key)" }); + return false; + } + return true; +} +function parseSolSecret(secretRaw) { + const s = String(secretRaw || "").trim(); + if (!s) throw new Error("Secret key kosong"); + + // JSON array: [12,34,...] + if (s.startsWith("[")) { + const arr = JSON.parse(s); + const u8 = Uint8Array.from(arr); + return Keypair.fromSecretKey(u8); + } + + // Base58 string + const u8 = bs58.decode(s); + return Keypair.fromSecretKey(u8); +} - res.json({ - address: wallet.address, - privateKey: wallet.privateKey +// serve UI +app.use(express.static(path.join(__dirname, "public"))); + +// ---------------- Status ---------------- +app.get("/api/health", (_req, res) => { + ok(res, { + status: "up", + solRpc: SOL_RPC, + apiKeyEnabled: !!API_KEY, + hasSolWallet: !!SOL_KP, + address: SOL_KP ? SOL_KP.publicKey.toBase58() : null, }); }); -// set wallet from PK -app.post("/set-wallet", (req, res) => { - const { privateKey } = req.body; +app.get("/api/sol/status", (_req, res) => { + ok(res, { + rpc: SOL_RPC, + apiKeyEnabled: !!API_KEY, + hasWallet: !!SOL_KP, + address: SOL_KP ? SOL_KP.publicKey.toBase58() : null, + addressMasked: SOL_KP ? mask(SOL_KP.publicKey.toBase58()) : null, + note: SOL_KP + ? "Wallet aktif (disimpan RAM)." + : "Wallet belum diset. Paste secret di UI > Set Wallet.", + }); +}); - try { - const wallet = new Wallet(privateKey, provider); - CURRENT_WALLET = wallet; +// ---------------- Setup/Clear wallet ---------------- +app.post("/api/sol/setup", (req, res) => { + if (!requireApiKey(req, res)) return; - res.json({ address: wallet.address }); - } catch { - res.json({ error: "Invalid private key" }); + try { + const secret = req.body?.secret; + const kp = parseSolSecret(secret); + SOL_KP = kp; + + const address = kp.publicKey.toBase58(); + ok(res, { + address, + addressMasked: mask(address), + note: "SOL wallet terset (disimpan di RAM). Restart server = set ulang.", + }); + } catch (e) { + fail(res, e.message || "Gagal setup SOL wallet"); } }); -// ================= AI ================= -async function aiDecision(token) { - const res = await fetch("https://api.groq.com/openai/v1/chat/completions", { - method: "POST", - headers: { - "Authorization": `Bearer ${GROQ_API_KEY}`, - "Content-Type": "application/json" - }, - body: JSON.stringify({ - model: "llama3-70b-8192", - messages: [ - { - role: "user", - content: `Analyze ${token}. Answer BUY / SELL / SKIP` - } - ] - }) - }); +app.post("/api/sol/clear", (req, res) => { + if (!requireApiKey(req, res)) return; + SOL_KP = null; + ok(res, { note: "SOL wallet dihapus dari memory." }); +}); - const data = await res.json(); - return data.choices[0].message.content; -} +// ---------------- Balance ---------------- +app.get("/api/sol/balance", async (req, res) => { + if (!requireApiKey(req, res)) return; -// ================= BALANCE ================= -app.get("/balance", async (req, res) => { try { - if (!CURRENT_WALLET) { - return res.json({ error: "Set wallet first" }); - } - - const evmBal = await provider.getBalance(CURRENT_WALLET.address); - - res.json({ - address: CURRENT_WALLET.address, - evm: ethers.formatEther(evmBal) + if (!SOL_KP) + return fail(res, "SOL wallet belum diset. Setup dulu di UI."); + + const lamports = await solConn.getBalance(SOL_KP.publicKey, "confirmed"); + ok(res, { + address: SOL_KP.publicKey.toBase58(), + sol: lamports / 1e9, + lamports, }); } catch (e) { - res.json({ error: e.message }); + fail(res, e.message || "Gagal ambil SOL balance"); } }); -// ================= SWAP ================= -app.post("/swap-evm", async (req, res) => { +// ---------------- Jupiter SWAP EXECUTE (server signs) ---------------- +// inputMint/outputMint: mint address +// amount: base units (lamports utk SOL/wSOL, atau token base units) +// slippageBps: default 50 (0.5%) +app.post("/api/sol/swap-execute", async (req, res) => { + if (!requireApiKey(req, res)) return; + try { - if (!CURRENT_WALLET) { - return res.json({ error: "Set wallet first" }); + if (!SOL_KP) + return fail(res, "SOL wallet belum diset. Setup dulu di UI."); + + const { inputMint, outputMint, amount, slippageBps = 50 } = req.body || {}; + if (!inputMint || !outputMint) + return fail(res, "inputMint/outputMint kosong"); + if (!amount) return fail(res, "amount kosong (base units/lamports)"); + + const userPublicKey = SOL_KP.publicKey.toBase58(); + + // 1) Quote + const qUrl = new URL("https://quote-api.jup.ag/v6/quote"); + qUrl.searchParams.set("inputMint", inputMint); + qUrl.searchParams.set("outputMint", outputMint); + qUrl.searchParams.set("amount", String(amount)); + qUrl.searchParams.set("slippageBps", String(slippageBps)); + + const quoteRes = await fetch(qUrl.toString()); + const quoteJson = await quoteRes.json(); + + if (!quoteJson?.routePlan) { + return fail(res, "Quote gagal / pair tidak ditemukan", { raw: quoteJson }); } - const r = await fetch("https://li.quest/v1/quote", { + // 2) Build swap tx + const swapRes = await fetch("https://quote-api.jup.ag/v6/swap", { method: "POST", - headers: {"Content-Type":"application/json"}, + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - fromChain: 1, - toChain: 1, - fromToken: "USDC", - toToken: "ETH", - fromAmount: "1000000", - fromAddress: CURRENT_WALLET.address - }) + quoteResponse: quoteJson, + userPublicKey, + wrapAndUnwrapSol: true, + dynamicComputeUnitLimit: true, + }), }); - const data = await r.json(); - const tx = data.transactionRequest; + const swapJson = await swapRes.json(); + if (!swapJson?.swapTransaction) { + return fail(res, "Swap TX gagal dibuat", { raw: swapJson }); + } - const txResponse = await CURRENT_WALLET.sendTransaction({ - to: tx.to, - data: tx.data, - value: tx.value - }); + // 3) Deserialize -> sign -> broadcast + const txBytes = Buffer.from(swapJson.swapTransaction, "base64"); + const vtx = VersionedTransaction.deserialize(txBytes); - res.json({ tx: txResponse.hash }); + vtx.sign([SOL_KP]); - } catch (e) { - res.json({ error: e.message }); - } -}); + const sig = await solConn.sendRawTransaction(vtx.serialize(), { + skipPreflight: false, + maxRetries: 3, + }); -// ================= AUTO TRADE ================= -app.post("/auto-trade", async (req, res) => { - try { - const decision = await aiDecision("ETH"); - res.json({ decision }); + const conf = await solConn.confirmTransaction(sig, "confirmed"); + + ok(res, { + signature: sig, + confirmation: conf?.value || null, + quote: { + inAmount: quoteJson.inAmount, + outAmount: quoteJson.outAmount, + priceImpactPct: quoteJson.priceImpactPct, + }, + note: "EXECUTED: tx ditandatangani server + broadcast ke jaringan.", + }); } catch (e) { - res.json({ error: e.message }); + fail(res, e.message || "Swap execute error"); } }); -// ================= START ================= app.listen(PORT, () => { - console.log(`🚀 RUNNING http://localhost:${PORT}`); + console.log(`✅ Running on http://0.0.0.0:${PORT}`); }); From fa400254911fe8c40d499fdc90e30f7cf0c6239c Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Wed, 18 Feb 2026 00:07:12 +0700 Subject: [PATCH 18/70] Update index.html --- public/index.html | 65 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/public/index.html b/public/index.html index 2e633c9..f762ec8 100644 --- a/public/index.html +++ b/public/index.html @@ -1,27 +1,52 @@ - + - - Trading Agent - - - + + + + Solana Swap Execute + + + +
+

🟣 Solana Swap Execute

+

Runtime setup: paste secret sekali → disimpan RAM (no .env needed)

-
-

🚀 Trading Agent

+
+

Security (Optional)

+ +

Kalau server public, wajib set API_KEY biar aman.

+
- +
+

Setup SOL Wallet

+ +
+ + + + +
+

Restart server = harus setup ulang.

+
-
- - - - - -
+
+

Swap (Jupiter Execute)

+
+ + +
+
+ + +
-
-
+ +

amount default 10,000,000 lamports = 0.01 SOL (wSOL mint)

+
- - +
Ready.
+
+ + + From 69b60cf88c64a304ecdd512267a2eda7b34e24b0 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Wed, 18 Feb 2026 00:08:02 +0700 Subject: [PATCH 19/70] Update style.css --- public/style.css | 51 ++++++++++++++++-------------------------------- 1 file changed, 17 insertions(+), 34 deletions(-) diff --git a/public/style.css b/public/style.css index 32e27ab..12001af 100644 --- a/public/style.css +++ b/public/style.css @@ -1,36 +1,19 @@ -body { - background: #0d1117; - color: white; - font-family: Arial; - text-align: center; +body { margin:0; font-family:system-ui, Arial; background:#0b0f14; color:#e8eef6; } +.container{ max-width:960px; margin:0 auto; padding:16px; } +h1{ margin:8px 0 4px; } +.muted{ color:#97a6b8; margin:0 0 10px; } +.card{ margin-top:12px; padding:12px; border:1px solid rgba(255,255,255,.08); border-radius:14px; background:rgba(255,255,255,.03); } +.row{ display:flex; gap:10px; flex-wrap:wrap; margin:10px 0; } +input, textarea{ + flex:1; min-width:240px; padding:10px; border-radius:12px; + border:1px solid rgba(255,255,255,.08); background:rgba(0,0,0,.25); color:#e8eef6; } - -.container { - margin-top: 100px; -} - -input { - padding: 10px; - width: 250px; - border-radius: 8px; - border: none; -} - -button { - margin: 8px; - padding: 10px 20px; - border-radius: 8px; - border: none; - cursor: pointer; - background: #238636; - color: white; -} - -button:hover { - background: #2ea043; -} - -#output { - margin-top: 20px; - font-size: 18px; +textarea{ min-height:120px; } +button{ + padding:10px 12px; border-radius:12px; cursor:pointer; font-weight:800; + border:1px solid rgba(168,85,247,.35); background:linear-gradient(#a855f7,#7c3aed); color:#0b0f14; } +button.ghost{ background:transparent; color:#e8eef6; border:1px solid rgba(255,255,255,.12); } +pre{ margin-top:12px; padding:12px; border-radius:14px; border:1px solid rgba(255,255,255,.08); + background:rgba(0,0,0,.25); white-space:pre-wrap; } +.hint{ color:#97a6b8; font-size:12px; margin:6px 0 0; } From d17f38440c647c0b98cf6f7ed7591c30cf91a541 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Wed, 18 Feb 2026 00:09:23 +0700 Subject: [PATCH 20/70] Update app.js --- public/app.js | 79 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 30 deletions(-) diff --git a/public/app.js b/public/app.js index 0e87ed8..781e364 100644 --- a/public/app.js +++ b/public/app.js @@ -1,46 +1,65 @@ -async function setWallet() { - const pk = document.getElementById("pk").value; +const out = document.getElementById("out"); +const apikey = document.getElementById("apikey"); +const secret = document.getElementById("secret"); - const res = await fetch("/set-wallet", { - method: "POST", - headers: {"Content-Type":"application/json"}, - body: JSON.stringify({ privateKey: pk }) - }); +function log(x){ + out.textContent = typeof x === "string" ? x : JSON.stringify(x, null, 2); +} - const data = await res.json(); +async function req(url, method = "GET", body) { + const headers = { "Content-Type": "application/json" }; + const key = apikey.value.trim(); + if (key) headers["x-api-key"] = key; - document.getElementById("output").innerText = - data.address || data.error; + const r = await fetch(url, { + method, + headers, + body: body ? JSON.stringify(body) : undefined + }); + + return r.json().catch(() => ({ ok:false, error:"Invalid JSON response" })); } -async function generateWallet() { - const res = await fetch("/generate-wallet"); - const data = await res.json(); +async function status() { + const r = await req("/api/sol/status"); + log(r); +} - document.getElementById("output").innerText = - `Address: ${data.address}\nPK: ${data.privateKey}`; +async function setup() { + const r = await req("/api/sol/setup", "POST", { secret: secret.value }); + log(r); } -async function getBalance() { - const res = await fetch("/balance"); - const data = await res.json(); +async function clearWallet() { + const r = await req("/api/sol/clear", "POST", {}); + log(r); +} - document.getElementById("output").innerText = - data.evm ? `Balance: ${data.evm} ETH` : data.error; +async function balance() { + const r = await req("/api/sol/balance"); + log(r); } async function swap() { - const res = await fetch("/swap-evm", { method: "POST" }); - const data = await res.json(); + const inputMint = document.getElementById("inMint").value.trim(); + const outputMint = document.getElementById("outMint").value.trim(); + const amount = document.getElementById("amount").value.trim(); + const slippageBps = Number(document.getElementById("slip").value.trim() || "50"); + + const r = await req("/api/sol/swap-execute", "POST", { + inputMint, + outputMint, + amount, + slippageBps + }); - document.getElementById("output").innerText = - data.tx || data.error; + log(r); } -async function autoTrade() { - const res = await fetch("/auto-trade", { method: "POST" }); - const data = await res.json(); +window.status = status; +window.setup = setup; +window.clearWallet = clearWallet; +window.balance = balance; +window.swap = swap; - document.getElementById("output").innerText = - data.decision || data.error; -} +status(); From 88a52cf033667a9cac64d70d18c0ce2b9e12e45c Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Wed, 18 Feb 2026 00:30:50 +0700 Subject: [PATCH 21/70] Update server.js --- server.js | 628 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 465 insertions(+), 163 deletions(-) diff --git a/server.js b/server.js index 15bd373..fb04e92 100644 --- a/server.js +++ b/server.js @@ -1,223 +1,525 @@ -import express from "express"; -import cors from "cors"; -import path from "path"; -import { fileURLToPath } from "url"; -import dotenv from "dotenv"; -import bs58 from "bs58"; +import 'dotenv/config'; +import express from 'express'; +import cors from 'cors'; +import bs58 from 'bs58'; +import { ethers } from 'ethers'; import { Connection, Keypair, VersionedTransaction, -} from "@solana/web3.js"; - -dotenv.config(); + PublicKey +} from '@solana/web3.js'; const app = express(); app.use(cors()); -app.use(express.json({ limit: "3mb" })); +app.use(express.json({ limit: '2mb' })); +app.use(express.static('public')); + +const API_KEY = process.env.API_KEY || ''; +const GROQ_API_KEY = process.env.GROQ_API_KEY || ''; +const GROQ_MODEL = process.env.GROQ_MODEL || 'llama-3.1-70b-versatile'; + +const RPC_ETH = process.env.EVM_RPC_ETH || ''; +const RPC_BSC = process.env.EVM_RPC_BSC || ''; +const RPC_BASE = process.env.EVM_RPC_BASE || ''; +const SOL_RPC = process.env.SOL_RPC || 'https://api.mainnet-beta.solana.com'; +const ZEROX_API_KEY = process.env.ZEROX_API_KEY || ''; + +const CHAIN = { + eth: { chainId: 1, name: 'Ethereum', rpc: RPC_ETH }, + bsc: { chainId: 56, name: 'BSC', rpc: RPC_BSC }, + base: { chainId: 8453, name: 'Base', rpc: RPC_BASE } +}; + +function requireApiKey(req) { + if (!API_KEY) return true; // if not set, allow (not recommended) + const k = req.headers['x-api-key']; + return k && k === API_KEY; +} -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); +function maskAddr(a) { + if (!a) return null; + if (a.length <= 10) return a; + return `${a.slice(0, 6)}…${a.slice(-4)}`; +} -const PORT = process.env.PORT || 3000; +function isEvmAddress(x) { + return typeof x === 'string' && /^0x[a-fA-F0-9]{40}$/.test(x.trim()); +} +function isProbablySolAddress(x) { + // simple base58-ish length heuristic + return typeof x === 'string' && x.trim().length >= 32 && x.trim().length <= 50 && !x.trim().startsWith('0x'); +} -// ===== SOLANA CONFIG ===== -const SOL_RPC = process.env.SOL_RPC || "https://api.mainnet-beta.solana.com"; -const solConn = new Connection(SOL_RPC, "confirmed"); +function parseSolSecret(input) { + const s = (input || '').trim(); + if (!s) throw new Error('Empty SOL secret'); + // JSON array + if (s.startsWith('[')) { + const arr = JSON.parse(s); + if (!Array.isArray(arr)) throw new Error('Invalid JSON array'); + return Keypair.fromSecretKey(Uint8Array.from(arr)); + } + // base58 secret key + const bytes = bs58.decode(s); + return Keypair.fromSecretKey(bytes); +} -// ===== OPTIONAL API KEY (recommended) ===== -const API_KEY = (process.env.API_KEY || "").trim(); +function parseEvmPk(input) { + const s = (input || '').trim(); + if (!s) throw new Error('Empty EVM private key'); + const pk = s.startsWith('0x') ? s : `0x${s}`; + if (!/^0x[a-fA-F0-9]{64}$/.test(pk)) throw new Error('Invalid EVM private key format'); + return pk; +} -// ===== IN-MEMORY SOL WALLET (RUNTIME SETUP) ===== -let SOL_KP = null; +// In-memory wallets (RAM only) +const mem = { + sol: { kp: null, address: null }, + evm: { + eth: { wallet: null, address: null }, + bsc: { wallet: null, address: null }, + base: { wallet: null, address: null } + } +}; -// ---------------- Helpers ---------------- -function ok(res, data) { - res.json({ ok: true, ...data }); -} -function fail(res, msg, extra = {}) { - res.status(400).json({ ok: false, error: msg, ...extra }); +// Providers +function getProvider(chainKey) { + const c = CHAIN[chainKey]; + if (!c?.rpc) throw new Error(`Missing RPC for ${chainKey}`); + return new ethers.JsonRpcProvider(c.rpc, c.chainId); } -function mask(addr) { - if (!addr) return ""; - return `${addr.slice(0, 6)}...${addr.slice(-4)}`; + +function riskGate({ chain, amountInHuman, slippageBps, mode }) { + // mode: 'swap' | 'bridge' + const issues = []; + const warnings = []; + + const amt = Number(amountInHuman); + const slip = Number(slippageBps); + + if (!Number.isFinite(amt) || amt <= 0) issues.push('Amount must be > 0'); + if (!Number.isFinite(slip) || slip <= 0) warnings.push('Slippage not set (or invalid). Consider 50–150 bps.'); + if (slip > 300) warnings.push('High slippage (>3%) — price impact / sandwich risk higher.'); + + // simple "big size" heuristics + if (chain === 'sol' && amt >= 1) warnings.push('Large SOL amount — use burner wallet, start small.'); + if ((chain === 'eth' || chain === 'base') && amt >= 0.2) warnings.push('Large amount — start with small test tx first.'); + if (chain === 'bsc' && amt >= 1) warnings.push('Large amount — start with small test tx first.'); + + if (mode === 'bridge') warnings.push('Bridge adds extra risk: route failures, delays, relayers, extra fees.'); + + return { ok: issues.length === 0, issues, warnings }; } -function requireApiKey(req, res) { - if (!API_KEY) return true; // unlocked if not set - const got = req.headers["x-api-key"]; - if (got !== API_KEY) { - res - .status(401) - .json({ ok: false, error: "Unauthorized (missing/invalid x-api-key)" }); - return false; + +async function groqParse(userText) { + // Output: strict JSON for routing + // (We still do regex fallback if Groq not set) + if (!GROQ_API_KEY) { + return { + intent: 'unknown', + chain: null, + action: null, + tokenIn: null, + tokenOut: null, + amount: null, + slippageBps: 100, + notes: ['GROQ_API_KEY not set — using fallback detector only.'] + }; } - return true; -} -function parseSolSecret(secretRaw) { - const s = String(secretRaw || "").trim(); - if (!s) throw new Error("Secret key kosong"); - // JSON array: [12,34,...] - if (s.startsWith("[")) { - const arr = JSON.parse(s); - const u8 = Uint8Array.from(arr); - return Keypair.fromSecretKey(u8); + const sys = `You are a routing agent for a crypto swap/bridge web app. +Return ONLY valid JSON with keys: +intent: "swap"|"bridge"|"balance"|"status"|"help"|"unknown" +chain: "sol"|"eth"|"bsc"|"base"|null +tokenIn: string|null (mint/address or "SOL"/"ETH" etc) +tokenOut: string|null +amount: string|null (human-readable, like "0.01") +slippageBps: number (default 100 if missing) +action: short string like "execute_swap"|"quote"|"execute_bridge"|"show_balance" +notes: array of short strings +Rules: +- If user includes an EVM address (0x...), assume EVM token. +- If user includes a Solana mint/address, assume sol. +- If chain not specified for EVM, set chain null. +- Keep slippageBps between 10 and 500. +No extra text.`; + + const body = { + model: GROQ_MODEL, + messages: [ + { role: 'system', content: sys }, + { role: 'user', content: userText } + ], + temperature: 0.2 + }; + + const r = await fetch('https://api.groq.com/openai/v1/chat/completions', { + method: 'POST', + headers: { + 'content-type': 'application/json', + authorization: `Bearer ${GROQ_API_KEY}` + }, + body: JSON.stringify(body) + }); + + if (!r.ok) { + const t = await r.text().catch(() => ''); + throw new Error(`Groq error: ${r.status} ${t}`); } - // Base58 string - const u8 = bs58.decode(s); - return Keypair.fromSecretKey(u8); + const j = await r.json(); + const content = j?.choices?.[0]?.message?.content?.trim() || ''; + // must be JSON only + return JSON.parse(content); } -// serve UI -app.use(express.static(path.join(__dirname, "public"))); - -// ---------------- Status ---------------- -app.get("/api/health", (_req, res) => { - ok(res, { - status: "up", - solRpc: SOL_RPC, +/* ======= API ======= */ + +app.get('/api/health', (req, res) => { + res.json({ + ok: true, + rpc: { + sol: SOL_RPC, + eth: !!RPC_ETH, + bsc: !!RPC_BSC, + base: !!RPC_BASE + }, + groq: !!GROQ_API_KEY, apiKeyEnabled: !!API_KEY, - hasSolWallet: !!SOL_KP, - address: SOL_KP ? SOL_KP.publicKey.toBase58() : null, + wallets: { + sol: !!mem.sol.kp, + eth: !!mem.evm.eth.wallet, + bsc: !!mem.evm.bsc.wallet, + base: !!mem.evm.base.wallet + } }); }); -app.get("/api/sol/status", (_req, res) => { - ok(res, { - rpc: SOL_RPC, - apiKeyEnabled: !!API_KEY, - hasWallet: !!SOL_KP, - address: SOL_KP ? SOL_KP.publicKey.toBase58() : null, - addressMasked: SOL_KP ? mask(SOL_KP.publicKey.toBase58()) : null, - note: SOL_KP - ? "Wallet aktif (disimpan RAM)." - : "Wallet belum diset. Paste secret di UI > Set Wallet.", - }); +/* --- Wallet setup --- */ +app.post('/api/wallet/sol', (req, res) => { + if (!requireApiKey(req)) return res.status(401).json({ ok: false, error: 'Unauthorized (x-api-key)' }); + try { + const kp = parseSolSecret(req.body?.secret); + mem.sol.kp = kp; + mem.sol.address = kp.publicKey.toBase58(); + res.json({ ok: true, address: mem.sol.address, addressMasked: maskAddr(mem.sol.address) }); + } catch (e) { + res.status(400).json({ ok: false, error: e.message }); + } }); -// ---------------- Setup/Clear wallet ---------------- -app.post("/api/sol/setup", (req, res) => { - if (!requireApiKey(req, res)) return; - +app.post('/api/wallet/evm', async (req, res) => { + if (!requireApiKey(req)) return res.status(401).json({ ok: false, error: 'Unauthorized (x-api-key)' }); try { - const secret = req.body?.secret; - const kp = parseSolSecret(secret); - SOL_KP = kp; - - const address = kp.publicKey.toBase58(); - ok(res, { - address, - addressMasked: mask(address), - note: "SOL wallet terset (disimpan di RAM). Restart server = set ulang.", - }); + const chain = (req.body?.chain || '').toLowerCase(); + if (!CHAIN[chain]) throw new Error('Invalid chain (use eth/bsc/base)'); + const pk = parseEvmPk(req.body?.privateKey); + const provider = getProvider(chain); + const wallet = new ethers.Wallet(pk, provider); + + mem.evm[chain].wallet = wallet; + mem.evm[chain].address = await wallet.getAddress(); + + res.json({ ok: true, chain, address: mem.evm[chain].address, addressMasked: maskAddr(mem.evm[chain].address) }); } catch (e) { - fail(res, e.message || "Gagal setup SOL wallet"); + res.status(400).json({ ok: false, error: e.message }); } }); -app.post("/api/sol/clear", (req, res) => { - if (!requireApiKey(req, res)) return; - SOL_KP = null; - ok(res, { note: "SOL wallet dihapus dari memory." }); -}); +/* --- Agent router --- */ +app.post('/api/agent', async (req, res) => { + try { + const text = String(req.body?.text || '').trim(); + if (!text) return res.json({ ok: true, route: { intent: 'unknown' }, risk: { ok: false, issues: ['Empty input'], warnings: [] } }); + + // Fallback detectors (always) + const has0x = /0x[a-fA-F0-9]{40}/.test(text); + const maybeSol = isProbablySolAddress(text) || /So11111111111111111111111111111111111111112/.test(text); + + let route = null; + try { + route = await groqParse(text); + } catch (e) { + route = { + intent: 'unknown', + chain: null, + action: null, + tokenIn: null, + tokenOut: null, + amount: null, + slippageBps: 100, + notes: [`Groq parse failed → fallback: ${e.message}`] + }; + } -// ---------------- Balance ---------------- -app.get("/api/sol/balance", async (req, res) => { - if (!requireApiKey(req, res)) return; + // Force chain inference if obvious + if (!route.chain) { + if (has0x) route.chain = 'eth'; // default; UI can switch to bsc/base + if (maybeSol) route.chain = 'sol'; + } - try { - if (!SOL_KP) - return fail(res, "SOL wallet belum diset. Setup dulu di UI."); - - const lamports = await solConn.getBalance(SOL_KP.publicKey, "confirmed"); - ok(res, { - address: SOL_KP.publicKey.toBase58(), - sol: lamports / 1e9, - lamports, - }); + // Basic risk if user asks swap/bridge + const mode = route.intent === 'bridge' ? 'bridge' : 'swap'; + const amountGuess = route.amount || '0'; + const slip = Math.min(500, Math.max(10, Number(route.slippageBps || 100))); + const risk = riskGate({ chain: route.chain || 'unknown', amountInHuman: amountGuess, slippageBps: slip, mode }); + + res.json({ ok: true, route: { ...route, slippageBps: slip }, risk }); } catch (e) { - fail(res, e.message || "Gagal ambil SOL balance"); + res.status(500).json({ ok: false, error: e.message }); } }); -// ---------------- Jupiter SWAP EXECUTE (server signs) ---------------- -// inputMint/outputMint: mint address -// amount: base units (lamports utk SOL/wSOL, atau token base units) -// slippageBps: default 50 (0.5%) -app.post("/api/sol/swap-execute", async (req, res) => { - if (!requireApiKey(req, res)) return; +/* ===== SOL (Jupiter Execute) ===== */ +app.get('/api/sol/balance', async (req, res) => { + try { + if (!mem.sol.kp) return res.status(400).json({ ok: false, error: 'SOL wallet not set' }); + const conn = new Connection(SOL_RPC, 'confirmed'); + const lamports = await conn.getBalance(mem.sol.kp.publicKey); + res.json({ ok: true, address: mem.sol.address, lamports, sol: lamports / 1e9 }); + } catch (e) { + res.status(500).json({ ok: false, error: e.message }); + } +}); +app.post('/api/sol/swap', async (req, res) => { + if (!requireApiKey(req)) return res.status(401).json({ ok: false, error: 'Unauthorized (x-api-key)' }); try { - if (!SOL_KP) - return fail(res, "SOL wallet belum diset. Setup dulu di UI."); + if (!mem.sol.kp) throw new Error('SOL wallet not set'); + + const { inputMint, outputMint, amountLamports, slippageBps } = req.body || {}; + if (!inputMint || !outputMint) throw new Error('Missing mint(s)'); + const amount = String(amountLamports || '').trim(); + if (!/^\d+$/.test(amount)) throw new Error('amountLamports must be integer string'); + const slip = Math.min(500, Math.max(10, Number(slippageBps || 100))); + + // Risk gate (simple) + const risk = riskGate({ chain: 'sol', amountInHuman: (Number(amount) / 1e9).toString(), slippageBps: slip, mode: 'swap' }); + if (!risk.ok) return res.status(400).json({ ok: false, error: 'Risk gate failed', risk }); + + // Quote + const qUrl = new URL('https://quote-api.jup.ag/v6/quote'); + qUrl.searchParams.set('inputMint', inputMint); + qUrl.searchParams.set('outputMint', outputMint); + qUrl.searchParams.set('amount', amount); + qUrl.searchParams.set('slippageBps', String(slip)); + + const qRes = await fetch(qUrl.toString()); + const quote = await qRes.json(); + if (!qRes.ok) throw new Error(`Jupiter quote failed: ${JSON.stringify(quote)}`); + + // Swap tx + const sRes = await fetch('https://quote-api.jup.ag/v6/swap', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + quoteResponse: quote, + userPublicKey: mem.sol.address, + wrapAndUnwrapSol: true, + dynamicComputeUnitLimit: true + }) + }); + const swapJson = await sRes.json(); + if (!sRes.ok) throw new Error(`Jupiter swap build failed: ${JSON.stringify(swapJson)}`); - const { inputMint, outputMint, amount, slippageBps = 50 } = req.body || {}; - if (!inputMint || !outputMint) - return fail(res, "inputMint/outputMint kosong"); - if (!amount) return fail(res, "amount kosong (base units/lamports)"); + const txBuf = Buffer.from(swapJson.swapTransaction, 'base64'); + const tx = VersionedTransaction.deserialize(txBuf); + tx.sign([mem.sol.kp]); - const userPublicKey = SOL_KP.publicKey.toBase58(); + const conn = new Connection(SOL_RPC, 'confirmed'); + const sig = await conn.sendTransaction(tx, { skipPreflight: false, maxRetries: 3 }); + const conf = await conn.confirmTransaction(sig, 'confirmed'); - // 1) Quote - const qUrl = new URL("https://quote-api.jup.ag/v6/quote"); - qUrl.searchParams.set("inputMint", inputMint); - qUrl.searchParams.set("outputMint", outputMint); - qUrl.searchParams.set("amount", String(amount)); - qUrl.searchParams.set("slippageBps", String(slippageBps)); + res.json({ ok: true, chain: 'sol', signature: sig, confirmation: conf, risk }); + } catch (e) { + res.status(400).json({ ok: false, error: e.message }); + } +}); - const quoteRes = await fetch(qUrl.toString()); - const quoteJson = await quoteRes.json(); +/* ===== EVM SWAP (0x Execute) ===== */ +async function zeroXQuote({ chainKey, sellToken, buyToken, sellAmountWei, slippageBps }) { + const c = CHAIN[chainKey]; + if (!c) throw new Error('Invalid chain'); + const base = `https://api.0x.org/swap/v1/quote`; + + const url = new URL(base); + url.searchParams.set('chainId', String(c.chainId)); + url.searchParams.set('sellToken', sellToken); + url.searchParams.set('buyToken', buyToken); + url.searchParams.set('sellAmount', sellAmountWei); + // 0x uses slippagePercentage (0-1) + const slipPct = Math.min(0.05, Math.max(0.001, Number(slippageBps || 100) / 10000)); + url.searchParams.set('slippagePercentage', String(slipPct)); + + const headers = { 'content-type': 'application/json' }; + if (ZEROX_API_KEY) headers['0x-api-key'] = ZEROX_API_KEY; + + const r = await fetch(url.toString(), { headers }); + const j = await r.json(); + if (!r.ok) throw new Error(`0x quote failed: ${JSON.stringify(j)}`); + return j; +} - if (!quoteJson?.routePlan) { - return fail(res, "Quote gagal / pair tidak ditemukan", { raw: quoteJson }); - } +app.get('/api/evm/balance', async (req, res) => { + try { + const chain = String(req.query.chain || '').toLowerCase(); + if (!CHAIN[chain]) throw new Error('Invalid chain'); + const w = mem.evm[chain].wallet; + if (!w) throw new Error(`EVM wallet not set for ${chain}`); + const bal = await w.provider.getBalance(await w.getAddress()); + res.json({ ok: true, chain, address: mem.evm[chain].address, nativeWei: bal.toString(), native: ethers.formatEther(bal) }); + } catch (e) { + res.status(400).json({ ok: false, error: e.message }); + } +}); - // 2) Build swap tx - const swapRes = await fetch("https://quote-api.jup.ag/v6/swap", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - quoteResponse: quoteJson, - userPublicKey, - wrapAndUnwrapSol: true, - dynamicComputeUnitLimit: true, - }), +app.post('/api/evm/swap', async (req, res) => { + if (!requireApiKey(req)) return res.status(401).json({ ok: false, error: 'Unauthorized (x-api-key)' }); + try { + const { chain, sellToken, buyToken, sellAmountWei, slippageBps } = req.body || {}; + const chainKey = String(chain || '').toLowerCase(); + if (!CHAIN[chainKey]) throw new Error('Invalid chain (eth/bsc/base)'); + const wallet = mem.evm[chainKey].wallet; + if (!wallet) throw new Error(`EVM wallet not set for ${chainKey}`); + + if (!sellToken || !buyToken) throw new Error('Missing token(s)'); + if (!/^\d+$/.test(String(sellAmountWei || ''))) throw new Error('sellAmountWei must be integer string'); + const slip = Math.min(500, Math.max(10, Number(slippageBps || 100))); + + // Risk gate (human amount unknown if token decimals unknown; we at least gate slippage) + const risk = riskGate({ chain: chainKey, amountInHuman: '0.1', slippageBps: slip, mode: 'swap' }); + if (!risk.ok) return res.status(400).json({ ok: false, error: 'Risk gate failed', risk }); + + const quote = await zeroXQuote({ + chainKey, + sellToken, + buyToken, + sellAmountWei: String(sellAmountWei), + slippageBps: slip }); - const swapJson = await swapRes.json(); - if (!swapJson?.swapTransaction) { - return fail(res, "Swap TX gagal dibuat", { raw: swapJson }); + // Approve if needed (ERC20 sells) + const isSellNative = sellToken.toUpperCase() === 'ETH' || sellToken.toUpperCase() === 'BNB' || sellToken.toUpperCase() === 'MATIC' || sellToken.toUpperCase() === 'AVAX'; + // Better rule: native tokens should be passed as 'ETH' on Ethereum/base and 'BNB' on BSC in 0x + if (!isSellNative && isEvmAddress(sellToken)) { + const erc20 = new ethers.Contract( + sellToken, + [ + 'function allowance(address owner, address spender) view returns (uint256)', + 'function approve(address spender, uint256 value) returns (bool)' + ], + wallet + ); + const owner = await wallet.getAddress(); + const spender = quote.allowanceTarget; + const allowance = await erc20.allowance(owner, spender); + const need = BigInt(quote.sellAmount); + if (BigInt(allowance.toString()) < need) { + const txa = await erc20.approve(spender, need); + await txa.wait(); + } } - // 3) Deserialize -> sign -> broadcast - const txBytes = Buffer.from(swapJson.swapTransaction, "base64"); - const vtx = VersionedTransaction.deserialize(txBytes); + // Send swap tx from quote + const tx = await wallet.sendTransaction({ + to: quote.to, + data: quote.data, + value: quote.value ? BigInt(quote.value) : 0n, + gasLimit: quote.gas ? BigInt(quote.gas) : undefined + }); + const receipt = await tx.wait(); + + res.json({ + ok: true, + chain: chainKey, + hash: tx.hash, + receipt: { + status: receipt.status, + blockNumber: receipt.blockNumber, + gasUsed: receipt.gasUsed?.toString?.() + }, + risk + }); + } catch (e) { + res.status(400).json({ ok: false, error: e.message }); + } +}); - vtx.sign([SOL_KP]); +/* ===== BRIDGE (EVM↔EVM via LI.FI) ===== */ +app.post('/api/bridge/evm', async (req, res) => { + if (!requireApiKey(req)) return res.status(401).json({ ok: false, error: 'Unauthorized (x-api-key)' }); + try { + const { fromChain, toChain, fromToken, toToken, fromAmountWei, slippageBps } = req.body || {}; + const a = String(fromChain || '').toLowerCase(); + const b = String(toChain || '').toLowerCase(); + if (!CHAIN[a] || !CHAIN[b]) throw new Error('Bridge supports only eth/bsc/base for now'); + if (!/^\d+$/.test(String(fromAmountWei || ''))) throw new Error('fromAmountWei must be integer string'); + const wallet = mem.evm[a].wallet; + if (!wallet) throw new Error(`EVM wallet not set for ${a}`); + const slip = Math.min(500, Math.max(10, Number(slippageBps || 100))); + + const risk = riskGate({ chain: a, amountInHuman: '0.1', slippageBps: slip, mode: 'bridge' }); + if (!risk.ok) return res.status(400).json({ ok: false, error: 'Risk gate failed', risk }); + + // LI.FI quote (route) + const rUrl = new URL('https://li.quest/v1/quote'); + rUrl.searchParams.set('fromChain', String(CHAIN[a].chainId)); + rUrl.searchParams.set('toChain', String(CHAIN[b].chainId)); + rUrl.searchParams.set('fromToken', fromToken); + rUrl.searchParams.set('toToken', toToken); + rUrl.searchParams.set('fromAmount', String(fromAmountWei)); + rUrl.searchParams.set('fromAddress', await wallet.getAddress()); + rUrl.searchParams.set('slippage', String(Math.min(0.05, Math.max(0.001, slip / 10000)))); + + const q = await fetch(rUrl.toString()); + const quote = await q.json(); + if (!q.ok) throw new Error(`LI.FI quote failed: ${JSON.stringify(quote)}`); + + const txReq = quote?.transactionRequest; + if (!txReq?.to || !txReq?.data) throw new Error('LI.FI did not return transactionRequest'); + + // approve if needed (ERC20) + if (isEvmAddress(fromToken) && quote?.estimate?.approvalAddress) { + const erc20 = new ethers.Contract( + fromToken, + ['function allowance(address owner, address spender) view returns (uint256)', 'function approve(address spender, uint256 value) returns (bool)'], + wallet + ); + const owner = await wallet.getAddress(); + const spender = quote.estimate.approvalAddress; + const allowance = await erc20.allowance(owner, spender); + const need = BigInt(fromAmountWei); + if (BigInt(allowance.toString()) < need) { + const txa = await erc20.approve(spender, need); + await txa.wait(); + } + } - const sig = await solConn.sendRawTransaction(vtx.serialize(), { - skipPreflight: false, - maxRetries: 3, + const tx = await wallet.sendTransaction({ + to: txReq.to, + data: txReq.data, + value: txReq.value ? BigInt(txReq.value) : 0n }); - const conf = await solConn.confirmTransaction(sig, "confirmed"); - - ok(res, { - signature: sig, - confirmation: conf?.value || null, - quote: { - inAmount: quoteJson.inAmount, - outAmount: quoteJson.outAmount, - priceImpactPct: quoteJson.priceImpactPct, - }, - note: "EXECUTED: tx ditandatangani server + broadcast ke jaringan.", + const receipt = await tx.wait(); + + res.json({ + ok: true, + mode: 'bridge', + fromChain: a, + toChain: b, + hash: tx.hash, + receipt: { status: receipt.status, blockNumber: receipt.blockNumber }, + risk, + note: 'Bridge finality depends on route. Track on LI.FI / explorer.' }); } catch (e) { - fail(res, e.message || "Swap execute error"); + res.status(400).json({ ok: false, error: e.message }); } }); -app.listen(PORT, () => { - console.log(`✅ Running on http://0.0.0.0:${PORT}`); +const PORT = process.env.PORT || 3000; +app.listen(PORT, '0.0.0.0', () => { + console.log(`RUNNING http://0.0.0.0:${PORT}`); }); From 8693179dc500c8c13c80598a272660b0871a7340 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Wed, 18 Feb 2026 00:34:30 +0700 Subject: [PATCH 22/70] Update package.json --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index bedaaf1..32a53e6 100644 --- a/package.json +++ b/package.json @@ -2,15 +2,16 @@ "name": "intercom-swap-by-pakeko", "version": "3.1.0", "type": "module", - "main": "server.js", + "private": true, "scripts": { "start": "node server.js" }, "dependencies": { - "@solana/web3.js": "^1.95.4", + "@solana/web3.js": "^1.98.0", "bs58": "^5.0.0", "cors": "^2.8.5", "dotenv": "^16.4.5", + "ethers": "^6.13.4", "express": "^4.19.2" } } From 2bb1091082eeb502f2e9b8b345a0591f7ad8b1b8 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Wed, 18 Feb 2026 00:35:23 +0700 Subject: [PATCH 23/70] Update .env.example --- .env.example | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index 3a58e8f..6fe0392 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,19 @@ -PORT=3000 +# ===== Security ===== +# Kalau server public, wajib isi ini + kirim header x-api-key di UI +API_KEY=change-me -# Solana RPC (default mainnet) -SOL_RPC=https://api.mainnet-beta.solana.com +# ===== Groq (OpenAI-compatible) ===== +GROQ_API_KEY=your_groq_key +GROQ_MODEL=llama-3.1-70b-versatile + +# ===== RPCs (wajib) ===== +EVM_RPC_ETH=https://eth-mainnet.g.alchemy.com/v2/KEY +EVM_RPC_BSC=https://bsc-dataseed.binance.org +EVM_RPC_BASE=https://mainnet.base.org -# Optional security gate (recommended) -# If set, you MUST send header x-api-key: for setup/swap/balance -API_KEY= +# ===== API 0x (optional tapi disaranin) ===== +# Beberapa endpoint bisa jalan tanpa, tapi stabil & rate limit lebih enak pakai key +ZEROX_API_KEY= +# ===== Solana RPC ===== +SOL_RPC=https://api.mainnet-beta.solana.com From bc881b103e73229c40be37533d966eb134678280 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Wed, 18 Feb 2026 00:37:07 +0700 Subject: [PATCH 24/70] Update index.html --- public/index.html | 361 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 311 insertions(+), 50 deletions(-) diff --git a/public/index.html b/public/index.html index f762ec8..b2cca21 100644 --- a/public/index.html +++ b/public/index.html @@ -1,52 +1,313 @@ - - - - - Solana Swap Execute - - - -
-

🟣 Solana Swap Execute

-

Runtime setup: paste secret sekali → disimpan RAM (no .env needed)

- -
-

Security (Optional)

- -

Kalau server public, wajib set API_KEY biar aman.

-
- -
-

Setup SOL Wallet

- -
- - - - -
-

Restart server = harus setup ulang.

-
- -
-

Swap (Jupiter Execute)

-
- - -
-
- - -
- - -

amount default 10,000,000 lamports = 0.01 SOL (wSOL mint)

-
- -
Ready.
-
- - - + + + + + IntercomSwap — ProMax + + + +
+
+
+
+
+

IntercomSwap — ProMax

+
Agent Router • Risk Gate • EVM Execute (0x) • SOL Execute (Jupiter) • Bridge (EVM↔EVM via LI.FI)
+
+
+
Checking…
+
+ +
+
+

Security (Recommended)

+ +
Tip: if server is public, set API_KEY and never paste main wallet keys.
+
+ +
+

Agent (Auto Detect + Risk)

+ +
+ + +
+
{}
+
{}
+
+
+ +
+ + + + +
+ + +
+
+

Setup SOL Wallet (RAM only)

+ +
+ + +
+
{}
+
+ +
+

Swap (Jupiter Execute)

+
+ + +
+
+ + + +
+
Start small. Slippage high increases sandwich risk.
+
{}
+
+
+ + +
+
+

Setup EVM Wallet (RAM only)

+
+ + +
+
+ + +
+
{}
+
+ +
+

EVM Swap Execute (0x)

+
+ + +
+
+ + + +
+
For ERC20 sells, approval is auto. For native sells use ETH (on eth/base) or BNB (on bsc).
+
{}
+
+
+ + +
+
+

Bridge Execute (EVM↔EVM via LI.FI)

+
+ + +
+
+ + +
+
+ + + +
+
Bridge risk: delays, route failures, extra fees. Start with tiny amount.
+
{}
+
+ +
+

SOL Bridge (Coming soon)

+
SOL↔EVM bridge needs Wormhole-style flow (VAA/relayers). Keeping this disabled to avoid fake “proof”.
+
{ "status": "disabled", "reason": "needs dedicated SOL↔EVM bridge implementation" }
+
+
+ + +
+
+

Risk / Safety Notes (Agent-enforced)

+
    +
  • Use burner wallets on public servers. Never paste your main wallet secret.
  • +
  • Start with tiny amount (test tx). Confirm output token + decimals.
  • +
  • Slippage: keep 0.5%–1.5% unless you know what you’re doing. High slippage increases MEV/sandwich risk.
  • +
  • Unknown tokens: can be honeypot / transfer-tax / blacklist. Agent will warn, but it’s not a guarantee.
  • +
  • Bridge: adds route/relayer risk. Expect delays, extra fees, and possible failures.
  • +
+
+
+ +
+ + + From 5051fed4e4c3b910fdb38adf9c1dc6b4355c85cd Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Wed, 18 Feb 2026 07:05:18 +0700 Subject: [PATCH 25/70] Update package.json --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 32a53e6..a44da5b 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,11 @@ { "name": "intercom-swap-by-pakeko", - "version": "3.1.0", + "version": "3.2.0", "type": "module", "private": true, "scripts": { - "start": "node server.js" + "start": "node server.js", + "cli": "node cli.js" }, "dependencies": { "@solana/web3.js": "^1.98.0", From cf5ecdcb674e9d1a0282c6889b77fd3a76530492 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Wed, 18 Feb 2026 07:06:16 +0700 Subject: [PATCH 26/70] Update .env.example --- .env.example | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index 6fe0392..77de82a 100644 --- a/.env.example +++ b/.env.example @@ -1,19 +1,25 @@ +# ===== Server bind (private/public) ===== +# Private VPS only: +# HOST=127.0.0.1 +# Public: +HOST=0.0.0.0 +PORT=3000 + # ===== Security ===== -# Kalau server public, wajib isi ini + kirim header x-api-key di UI +# If server is public, SET THIS and use it in UI/CLI as x-api-key API_KEY=change-me # ===== Groq (OpenAI-compatible) ===== GROQ_API_KEY=your_groq_key GROQ_MODEL=llama-3.1-70b-versatile -# ===== RPCs (wajib) ===== +# ===== RPCs (required for execute) ===== EVM_RPC_ETH=https://eth-mainnet.g.alchemy.com/v2/KEY EVM_RPC_BSC=https://bsc-dataseed.binance.org EVM_RPC_BASE=https://mainnet.base.org -# ===== API 0x (optional tapi disaranin) ===== -# Beberapa endpoint bisa jalan tanpa, tapi stabil & rate limit lebih enak pakai key -ZEROX_API_KEY= - # ===== Solana RPC ===== SOL_RPC=https://api.mainnet-beta.solana.com + +# ===== Optional ===== +ZEROX_API_KEY= From 9ca292603cab7a76706d4c53a4bb6dd094ac50c0 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Wed, 18 Feb 2026 07:07:05 +0700 Subject: [PATCH 27/70] Update server.js --- server.js | 206 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 121 insertions(+), 85 deletions(-) diff --git a/server.js b/server.js index fb04e92..4aeebd2 100644 --- a/server.js +++ b/server.js @@ -3,12 +3,7 @@ import express from 'express'; import cors from 'cors'; import bs58 from 'bs58'; import { ethers } from 'ethers'; -import { - Connection, - Keypair, - VersionedTransaction, - PublicKey -} from '@solana/web3.js'; +import { Connection, Keypair, VersionedTransaction } from '@solana/web3.js'; const app = express(); app.use(cors()); @@ -26,9 +21,9 @@ const SOL_RPC = process.env.SOL_RPC || 'https://api.mainnet-beta.solana.com'; const ZEROX_API_KEY = process.env.ZEROX_API_KEY || ''; const CHAIN = { - eth: { chainId: 1, name: 'Ethereum', rpc: RPC_ETH }, - bsc: { chainId: 56, name: 'BSC', rpc: RPC_BSC }, - base: { chainId: 8453, name: 'Base', rpc: RPC_BASE } + eth: { chainId: 1, name: 'Ethereum', rpc: RPC_ETH, native: 'ETH' }, + bsc: { chainId: 56, name: 'BSC', rpc: RPC_BSC, native: 'BNB' }, + base: { chainId: 8453, name: 'Base', rpc: RPC_BASE, native: 'ETH' } }; function requireApiKey(req) { @@ -39,28 +34,25 @@ function requireApiKey(req) { function maskAddr(a) { if (!a) return null; - if (a.length <= 10) return a; - return `${a.slice(0, 6)}…${a.slice(-4)}`; + return a.length <= 10 ? a : `${a.slice(0, 6)}…${a.slice(-4)}`; } function isEvmAddress(x) { return typeof x === 'string' && /^0x[a-fA-F0-9]{40}$/.test(x.trim()); } + function isProbablySolAddress(x) { - // simple base58-ish length heuristic return typeof x === 'string' && x.trim().length >= 32 && x.trim().length <= 50 && !x.trim().startsWith('0x'); } function parseSolSecret(input) { const s = (input || '').trim(); if (!s) throw new Error('Empty SOL secret'); - // JSON array if (s.startsWith('[')) { const arr = JSON.parse(s); if (!Array.isArray(arr)) throw new Error('Invalid JSON array'); return Keypair.fromSecretKey(Uint8Array.from(arr)); } - // base58 secret key const bytes = bs58.decode(s); return Keypair.fromSecretKey(bytes); } @@ -73,7 +65,7 @@ function parseEvmPk(input) { return pk; } -// In-memory wallets (RAM only) +// RAM-only wallets const mem = { sol: { kp: null, address: null }, evm: { @@ -83,7 +75,6 @@ const mem = { } }; -// Providers function getProvider(chainKey) { const c = CHAIN[chainKey]; if (!c?.rpc) throw new Error(`Missing RPC for ${chainKey}`); @@ -91,7 +82,6 @@ function getProvider(chainKey) { } function riskGate({ chain, amountInHuman, slippageBps, mode }) { - // mode: 'swap' | 'bridge' const issues = []; const warnings = []; @@ -99,22 +89,19 @@ function riskGate({ chain, amountInHuman, slippageBps, mode }) { const slip = Number(slippageBps); if (!Number.isFinite(amt) || amt <= 0) issues.push('Amount must be > 0'); - if (!Number.isFinite(slip) || slip <= 0) warnings.push('Slippage not set (or invalid). Consider 50–150 bps.'); - if (slip > 300) warnings.push('High slippage (>3%) — price impact / sandwich risk higher.'); + if (!Number.isFinite(slip) || slip <= 0) warnings.push('Slippage not set/invalid. Try 50–150 bps.'); + if (slip > 300) warnings.push('High slippage (>3%) — higher MEV/sandwich risk.'); - // simple "big size" heuristics - if (chain === 'sol' && amt >= 1) warnings.push('Large SOL amount — use burner wallet, start small.'); - if ((chain === 'eth' || chain === 'base') && amt >= 0.2) warnings.push('Large amount — start with small test tx first.'); - if (chain === 'bsc' && amt >= 1) warnings.push('Large amount — start with small test tx first.'); + if (chain === 'sol' && amt >= 1) warnings.push('Large SOL amount — start small, use burner wallet.'); + if ((chain === 'eth' || chain === 'base') && amt >= 0.2) warnings.push('Large amount — do a tiny test tx first.'); + if (chain === 'bsc' && amt >= 1) warnings.push('Large amount — do a tiny test tx first.'); - if (mode === 'bridge') warnings.push('Bridge adds extra risk: route failures, delays, relayers, extra fees.'); + if (mode === 'bridge') warnings.push('Bridge risk: route failures/delays/extra fees.'); return { ok: issues.length === 0, issues, warnings }; } async function groqParse(userText) { - // Output: strict JSON for routing - // (We still do regex fallback if Groq not set) if (!GROQ_API_KEY) { return { intent: 'unknown', @@ -124,25 +111,24 @@ async function groqParse(userText) { tokenOut: null, amount: null, slippageBps: 100, - notes: ['GROQ_API_KEY not set — using fallback detector only.'] + notes: ['GROQ_API_KEY not set — fallback detectors only.'] }; } - const sys = `You are a routing agent for a crypto swap/bridge web app. -Return ONLY valid JSON with keys: + const sys = `Return ONLY valid JSON with keys: intent: "swap"|"bridge"|"balance"|"status"|"help"|"unknown" chain: "sol"|"eth"|"bsc"|"base"|null -tokenIn: string|null (mint/address or "SOL"/"ETH" etc) +tokenIn: string|null tokenOut: string|null -amount: string|null (human-readable, like "0.01") -slippageBps: number (default 100 if missing) -action: short string like "execute_swap"|"quote"|"execute_bridge"|"show_balance" -notes: array of short strings +amount: string|null +slippageBps: number +action: "execute_swap"|"execute_bridge"|"show_balance"|"quote"|"unknown" +notes: array of strings Rules: -- If user includes an EVM address (0x...), assume EVM token. -- If user includes a Solana mint/address, assume sol. -- If chain not specified for EVM, set chain null. -- Keep slippageBps between 10 and 500. +- If you see 0x... address, assume EVM token. +- If you see Solana mint/address, assume sol. +- If chain missing for EVM, set chain null. +- slippageBps clamp 10..500 No extra text.`; const body = { @@ -170,23 +156,22 @@ No extra text.`; const j = await r.json(); const content = j?.choices?.[0]?.message?.content?.trim() || ''; - // must be JSON only return JSON.parse(content); } -/* ======= API ======= */ +/* ================= API ================= */ app.get('/api/health', (req, res) => { res.json({ ok: true, + apiKeyEnabled: !!API_KEY, + groq: !!GROQ_API_KEY, rpc: { sol: SOL_RPC, eth: !!RPC_ETH, bsc: !!RPC_BSC, base: !!RPC_BASE }, - groq: !!GROQ_API_KEY, - apiKeyEnabled: !!API_KEY, wallets: { sol: !!mem.sol.kp, eth: !!mem.evm.eth.wallet, @@ -196,7 +181,37 @@ app.get('/api/health', (req, res) => { }); }); -/* --- Wallet setup --- */ +/* -------- Generate burner wallets (server-side) -------- */ +app.post('/api/gen/sol', (req, res) => { + if (!requireApiKey(req)) return res.status(401).json({ ok: false, error: 'Unauthorized (x-api-key)' }); + try { + const kp = Keypair.generate(); + res.json({ + ok: true, + address: kp.publicKey.toBase58(), + secretJson: JSON.stringify(Array.from(kp.secretKey)), + secretBase58: bs58.encode(kp.secretKey) + }); + } catch (e) { + res.status(500).json({ ok: false, error: e.message }); + } +}); + +app.post('/api/gen/evm', (req, res) => { + if (!requireApiKey(req)) return res.status(401).json({ ok: false, error: 'Unauthorized (x-api-key)' }); + try { + const w = ethers.Wallet.createRandom(); + res.json({ + ok: true, + address: w.address, + privateKey: w.privateKey + }); + } catch (e) { + res.status(500).json({ ok: false, error: e.message }); + } +}); + +/* -------- Wallet setup -------- */ app.post('/api/wallet/sol', (req, res) => { if (!requireApiKey(req)) return res.status(401).json({ ok: false, error: 'Unauthorized (x-api-key)' }); try { @@ -212,7 +227,7 @@ app.post('/api/wallet/sol', (req, res) => { app.post('/api/wallet/evm', async (req, res) => { if (!requireApiKey(req)) return res.status(401).json({ ok: false, error: 'Unauthorized (x-api-key)' }); try { - const chain = (req.body?.chain || '').toLowerCase(); + const chain = String(req.body?.chain || '').toLowerCase(); if (!CHAIN[chain]) throw new Error('Invalid chain (use eth/bsc/base)'); const pk = parseEvmPk(req.body?.privateKey); const provider = getProvider(chain); @@ -221,23 +236,33 @@ app.post('/api/wallet/evm', async (req, res) => { mem.evm[chain].wallet = wallet; mem.evm[chain].address = await wallet.getAddress(); - res.json({ ok: true, chain, address: mem.evm[chain].address, addressMasked: maskAddr(mem.evm[chain].address) }); + res.json({ + ok: true, + chain, + address: mem.evm[chain].address, + addressMasked: maskAddr(mem.evm[chain].address) + }); } catch (e) { res.status(400).json({ ok: false, error: e.message }); } }); -/* --- Agent router --- */ +/* -------- Agent router (detect + risk) -------- */ app.post('/api/agent', async (req, res) => { try { const text = String(req.body?.text || '').trim(); - if (!text) return res.json({ ok: true, route: { intent: 'unknown' }, risk: { ok: false, issues: ['Empty input'], warnings: [] } }); + if (!text) { + return res.json({ + ok: true, + route: { intent: 'unknown', chain: null }, + risk: { ok: false, issues: ['Empty input'], warnings: [] } + }); + } - // Fallback detectors (always) const has0x = /0x[a-fA-F0-9]{40}/.test(text); const maybeSol = isProbablySolAddress(text) || /So11111111111111111111111111111111111111112/.test(text); - let route = null; + let route; try { route = await groqParse(text); } catch (e) { @@ -255,23 +280,38 @@ app.post('/api/agent', async (req, res) => { // Force chain inference if obvious if (!route.chain) { - if (has0x) route.chain = 'eth'; // default; UI can switch to bsc/base if (maybeSol) route.chain = 'sol'; + else if (has0x) route.chain = 'base'; // default to BASE (as you wanted) } - // Basic risk if user asks swap/bridge + // Clamp slippage + const slip = Math.min(500, Math.max(10, Number(route.slippageBps || 100))); + route.slippageBps = slip; + + // Decide mode for risk const mode = route.intent === 'bridge' ? 'bridge' : 'swap'; const amountGuess = route.amount || '0'; - const slip = Math.min(500, Math.max(10, Number(route.slippageBps || 100))); - const risk = riskGate({ chain: route.chain || 'unknown', amountInHuman: amountGuess, slippageBps: slip, mode }); - res.json({ ok: true, route: { ...route, slippageBps: slip }, risk }); + const risk = riskGate({ + chain: route.chain || 'unknown', + amountInHuman: amountGuess, + slippageBps: slip, + mode + }); + + // Extra warnings if token looks unknown/suspicious + if (route.intent === 'swap' && route.chain && route.chain !== 'sol' && route.tokenOut && isEvmAddress(route.tokenOut)) { + risk.warnings.push('Unknown EVM token address — could be honeypot/tax/blacklist. Verify before executing.'); + } + + res.json({ ok: true, route, risk }); } catch (e) { res.status(500).json({ ok: false, error: e.message }); } }); -/* ===== SOL (Jupiter Execute) ===== */ +/* ================= SOL (Jupiter Execute) ================= */ + app.get('/api/sol/balance', async (req, res) => { try { if (!mem.sol.kp) return res.status(400).json({ ok: false, error: 'SOL wallet not set' }); @@ -294,11 +334,10 @@ app.post('/api/sol/swap', async (req, res) => { if (!/^\d+$/.test(amount)) throw new Error('amountLamports must be integer string'); const slip = Math.min(500, Math.max(10, Number(slippageBps || 100))); - // Risk gate (simple) - const risk = riskGate({ chain: 'sol', amountInHuman: (Number(amount) / 1e9).toString(), slippageBps: slip, mode: 'swap' }); + const human = (Number(amount) / 1e9).toString(); + const risk = riskGate({ chain: 'sol', amountInHuman: human, slippageBps: slip, mode: 'swap' }); if (!risk.ok) return res.status(400).json({ ok: false, error: 'Risk gate failed', risk }); - // Quote const qUrl = new URL('https://quote-api.jup.ag/v6/quote'); qUrl.searchParams.set('inputMint', inputMint); qUrl.searchParams.set('outputMint', outputMint); @@ -309,7 +348,6 @@ app.post('/api/sol/swap', async (req, res) => { const quote = await qRes.json(); if (!qRes.ok) throw new Error(`Jupiter quote failed: ${JSON.stringify(quote)}`); - // Swap tx const sRes = await fetch('https://quote-api.jup.ag/v6/swap', { method: 'POST', headers: { 'content-type': 'application/json' }, @@ -337,18 +375,17 @@ app.post('/api/sol/swap', async (req, res) => { } }); -/* ===== EVM SWAP (0x Execute) ===== */ +/* ================= EVM SWAP (0x Execute) ================= */ + async function zeroXQuote({ chainKey, sellToken, buyToken, sellAmountWei, slippageBps }) { const c = CHAIN[chainKey]; if (!c) throw new Error('Invalid chain'); - const base = `https://api.0x.org/swap/v1/quote`; - - const url = new URL(base); + const url = new URL('https://api.0x.org/swap/v1/quote'); url.searchParams.set('chainId', String(c.chainId)); url.searchParams.set('sellToken', sellToken); url.searchParams.set('buyToken', buyToken); url.searchParams.set('sellAmount', sellAmountWei); - // 0x uses slippagePercentage (0-1) + const slipPct = Math.min(0.05, Math.max(0.001, Number(slippageBps || 100) / 10000)); url.searchParams.set('slippagePercentage', String(slipPct)); @@ -385,9 +422,10 @@ app.post('/api/evm/swap', async (req, res) => { if (!sellToken || !buyToken) throw new Error('Missing token(s)'); if (!/^\d+$/.test(String(sellAmountWei || ''))) throw new Error('sellAmountWei must be integer string'); + const slip = Math.min(500, Math.max(10, Number(slippageBps || 100))); - // Risk gate (human amount unknown if token decimals unknown; we at least gate slippage) + // Basic risk gate (slippage focused; wei-human depends on decimals) const risk = riskGate({ chain: chainKey, amountInHuman: '0.1', slippageBps: slip, mode: 'swap' }); if (!risk.ok) return res.status(400).json({ ok: false, error: 'Risk gate failed', risk }); @@ -399,10 +437,9 @@ app.post('/api/evm/swap', async (req, res) => { slippageBps: slip }); - // Approve if needed (ERC20 sells) - const isSellNative = sellToken.toUpperCase() === 'ETH' || sellToken.toUpperCase() === 'BNB' || sellToken.toUpperCase() === 'MATIC' || sellToken.toUpperCase() === 'AVAX'; - // Better rule: native tokens should be passed as 'ETH' on Ethereum/base and 'BNB' on BSC in 0x - if (!isSellNative && isEvmAddress(sellToken)) { + // Approve if ERC20 sell + const isNativeSell = (sellToken || '').toUpperCase() === 'ETH' || (sellToken || '').toUpperCase() === 'BNB'; + if (!isNativeSell && isEvmAddress(sellToken)) { const erc20 = new ethers.Contract( sellToken, [ @@ -421,7 +458,6 @@ app.post('/api/evm/swap', async (req, res) => { } } - // Send swap tx from quote const tx = await wallet.sendTransaction({ to: quote.to, data: quote.data, @@ -434,11 +470,7 @@ app.post('/api/evm/swap', async (req, res) => { ok: true, chain: chainKey, hash: tx.hash, - receipt: { - status: receipt.status, - blockNumber: receipt.blockNumber, - gasUsed: receipt.gasUsed?.toString?.() - }, + receipt: { status: receipt.status, blockNumber: receipt.blockNumber }, risk }); } catch (e) { @@ -446,23 +478,24 @@ app.post('/api/evm/swap', async (req, res) => { } }); -/* ===== BRIDGE (EVM↔EVM via LI.FI) ===== */ +/* ================= BRIDGE (EVM↔EVM via LI.FI) ================= */ + app.post('/api/bridge/evm', async (req, res) => { if (!requireApiKey(req)) return res.status(401).json({ ok: false, error: 'Unauthorized (x-api-key)' }); try { const { fromChain, toChain, fromToken, toToken, fromAmountWei, slippageBps } = req.body || {}; const a = String(fromChain || '').toLowerCase(); const b = String(toChain || '').toLowerCase(); - if (!CHAIN[a] || !CHAIN[b]) throw new Error('Bridge supports only eth/bsc/base for now'); + if (!CHAIN[a] || !CHAIN[b]) throw new Error('Bridge supports only eth/bsc/base'); if (!/^\d+$/.test(String(fromAmountWei || ''))) throw new Error('fromAmountWei must be integer string'); + const wallet = mem.evm[a].wallet; if (!wallet) throw new Error(`EVM wallet not set for ${a}`); - const slip = Math.min(500, Math.max(10, Number(slippageBps || 100))); + const slip = Math.min(500, Math.max(10, Number(slippageBps || 100))); const risk = riskGate({ chain: a, amountInHuman: '0.1', slippageBps: slip, mode: 'bridge' }); if (!risk.ok) return res.status(400).json({ ok: false, error: 'Risk gate failed', risk }); - // LI.FI quote (route) const rUrl = new URL('https://li.quest/v1/quote'); rUrl.searchParams.set('fromChain', String(CHAIN[a].chainId)); rUrl.searchParams.set('toChain', String(CHAIN[b].chainId)); @@ -477,9 +510,9 @@ app.post('/api/bridge/evm', async (req, res) => { if (!q.ok) throw new Error(`LI.FI quote failed: ${JSON.stringify(quote)}`); const txReq = quote?.transactionRequest; - if (!txReq?.to || !txReq?.data) throw new Error('LI.FI did not return transactionRequest'); + if (!txReq?.to || !txReq?.data) throw new Error('LI.FI missing transactionRequest'); - // approve if needed (ERC20) + // approve if ERC20 if (isEvmAddress(fromToken) && quote?.estimate?.approvalAddress) { const erc20 = new ethers.Contract( fromToken, @@ -501,7 +534,6 @@ app.post('/api/bridge/evm', async (req, res) => { data: txReq.data, value: txReq.value ? BigInt(txReq.value) : 0n }); - const receipt = await tx.wait(); res.json({ @@ -512,14 +544,18 @@ app.post('/api/bridge/evm', async (req, res) => { hash: tx.hash, receipt: { status: receipt.status, blockNumber: receipt.blockNumber }, risk, - note: 'Bridge finality depends on route. Track on LI.FI / explorer.' + note: 'Bridge finality depends on route. Track on explorer / LI.FI.' }); } catch (e) { res.status(400).json({ ok: false, error: e.message }); } }); +/* ================= start ================= */ + const PORT = process.env.PORT || 3000; -app.listen(PORT, '0.0.0.0', () => { - console.log(`RUNNING http://0.0.0.0:${PORT}`); +const HOST = process.env.HOST || '0.0.0.0'; + +app.listen(PORT, HOST, () => { + console.log(`RUNNING http://${HOST}:${PORT}`); }); From 85c5a9de5a57548ea21993c68918e59c98486c27 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Wed, 18 Feb 2026 07:08:00 +0700 Subject: [PATCH 28/70] Update index.html --- public/index.html | 249 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 200 insertions(+), 49 deletions(-) diff --git a/public/index.html b/public/index.html index b2cca21..f75ac9a 100644 --- a/public/index.html +++ b/public/index.html @@ -32,6 +32,11 @@ .ok{color:var(--ok)} .bad{color:var(--danger)} pre{margin:10px 0 0;background:#0c121c;border:1px solid rgba(255,255,255,.08);border-radius:12px;padding:10px;overflow:auto;font-size:12px} .hide{display:none} + .inline{display:flex; gap:8px; align-items:center} + .eye{padding:10px 12px;border-radius:12px} + .warn{border:1px solid rgba(239,68,68,.35); background:rgba(239,68,68,.08); padding:10px; border-radius:12px; font-size:12px} + .good{border:1px solid rgba(34,197,94,.35); background:rgba(34,197,94,.08); padding:10px; border-radius:12px; font-size:12px} + .kbd{padding:2px 8px;border-radius:999px;border:1px solid rgba(255,255,255,.10);font-size:12px;color:#cfe2ff} @@ -49,18 +54,29 @@

IntercomSwap — ProMax

-

Security (Recommended)

- -
Tip: if server is public, set API_KEY and never paste main wallet keys.
+

Security (Masked ***** + Save local)

+
+ + +
+
Tip: Public server? Keep API_KEY ON. Use burner wallets only.
+
API key auto-saved in your browser (localStorage).
-

Agent (Auto Detect + Risk)

- +

Agent (Detect → Risk → Auto Fill)

+ +
+
+ +
{}
{}
@@ -76,8 +92,15 @@

Agent (Auto Detect + Risk)

-

Setup SOL Wallet (RAM only)

- +

SOL Wallet (Masked secret + Generator)

+ +
+ + +
+ + +
@@ -88,15 +111,15 @@

Setup SOL Wallet (RAM only)

Swap (Jupiter Execute)

- +
- - - + + +
-
Start small. Slippage high increases sandwich risk.
+
Agent risk gate will warn you before execution.
{}
@@ -104,15 +127,26 @@

Swap (Jupiter Execute)

-

Setup EVM Wallet (RAM only)

+

EVM Wallet (Masked PK + Generator)

+ +
+ + +
+
- + +
+ + +
+
@@ -129,9 +163,9 @@

EVM Swap Execute (0x)

- +
-
For ERC20 sells, approval is auto. For native sells use ETH (on eth/base) or BNB (on bsc).
+
BSC native = BNB, ETH/Base native = ETH.
{}
@@ -143,8 +177,8 @@

Bridge Execute (EVM↔EVM via LI.FI)

- - + +
- +
-
Bridge risk: delays, route failures, extra fees. Start with tiny amount.
+
Bridge risk is higher than swaps. Start tiny.
{}
-

SOL Bridge (Coming soon)

-
SOL↔EVM bridge needs Wormhole-style flow (VAA/relayers). Keeping this disabled to avoid fake “proof”.
-
{ "status": "disabled", "reason": "needs dedicated SOL↔EVM bridge implementation" }
+

SOL↔EVM Bridge

+
Disabled (coming soon). SOL↔EVM bridge needs Wormhole-style flow (VAA/relayers). Kept off to avoid fake proof.
+
{ "status": "disabled" }
-

Risk / Safety Notes (Agent-enforced)

+

Safety Notes (Risk Gate)

    -
  • Use burner wallets on public servers. Never paste your main wallet secret.
  • -
  • Start with tiny amount (test tx). Confirm output token + decimals.
  • -
  • Slippage: keep 0.5%–1.5% unless you know what you’re doing. High slippage increases MEV/sandwich risk.
  • -
  • Unknown tokens: can be honeypot / transfer-tax / blacklist. Agent will warn, but it’s not a guarantee.
  • -
  • Bridge: adds route/relayer risk. Expect delays, extra fees, and possible failures.
  • +
  • Use burner wallets on public servers. Never paste main wallet secrets.
  • +
  • Start tiny and do a test tx first.
  • +
  • High slippage increases MEV/sandwich risk.
  • +
  • Unknown tokens may be honeypot/tax/blacklist. Verify before executing.
  • +
  • Bridge adds route risk, delays, extra fees.
@@ -190,49 +224,147 @@

Risk / Safety Notes (Agent-enforced)

- - diff --git a/public/style.css b/public/style.css deleted file mode 100644 index 12001af..0000000 --- a/public/style.css +++ /dev/null @@ -1,19 +0,0 @@ -body { margin:0; font-family:system-ui, Arial; background:#0b0f14; color:#e8eef6; } -.container{ max-width:960px; margin:0 auto; padding:16px; } -h1{ margin:8px 0 4px; } -.muted{ color:#97a6b8; margin:0 0 10px; } -.card{ margin-top:12px; padding:12px; border:1px solid rgba(255,255,255,.08); border-radius:14px; background:rgba(255,255,255,.03); } -.row{ display:flex; gap:10px; flex-wrap:wrap; margin:10px 0; } -input, textarea{ - flex:1; min-width:240px; padding:10px; border-radius:12px; - border:1px solid rgba(255,255,255,.08); background:rgba(0,0,0,.25); color:#e8eef6; -} -textarea{ min-height:120px; } -button{ - padding:10px 12px; border-radius:12px; cursor:pointer; font-weight:800; - border:1px solid rgba(168,85,247,.35); background:linear-gradient(#a855f7,#7c3aed); color:#0b0f14; -} -button.ghost{ background:transparent; color:#e8eef6; border:1px solid rgba(255,255,255,.12); } -pre{ margin-top:12px; padding:12px; border-radius:14px; border:1px solid rgba(255,255,255,.08); - background:rgba(0,0,0,.25); white-space:pre-wrap; } -.hint{ color:#97a6b8; font-size:12px; margin:6px 0 0; } From 5c91351e635a46034091f6a7efd01225f6585860 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Thu, 19 Feb 2026 08:41:36 +0700 Subject: [PATCH 39/70] Update package.json --- package.json | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index a40e49d..96ccf94 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,20 @@ { - "name": "intercom-swap-by-pakeko", - "version": "4.0.0", + "name": "intercom-cli-final", + "version": "5.0.0", "type": "module", - "private": true, - "description": "Intercom-style CLI + Web API for swap/bridge with safe DRY_RUN test mode.", - "main": "server.js", + "main": "src/cli/index.js", "scripts": { - "start": "node server.js", - "web": "node server.js", - "cli": "node cli.js", - "mode": "bash -lc 'echo \"\\nChoose mode:\\n 1) Web (server)\\n 2) CLI\\n\"; read -p \"Select (1/2): \" opt; if [ \"$opt\" = \"1\" ]; then node server.js; else node cli.js; fi'", - "health": "node -e \"import('dotenv/config'); console.log('MODE=',process.env.MODE,'DRY_RUN=',process.env.DRY_RUN)\"" + "cli": "node src/cli/index.js", + "quote": "node src/cli/index.js quote", + "swap": "node src/cli/index.js swap", + "agent": "node src/cli/index.js agent" }, "dependencies": { - "@solana/web3.js": "^1.95.0", + "@solana/web3.js": "^1.98.4", "bs58": "^5.0.0", - "cors": "^2.8.5", "dotenv": "^16.4.5", - "ethers": "^6.13.4", - "express": "^4.19.2" + "ethers": "^6.13.0", + "undici": "^6.19.8", + "yargs": "^17.7.2" } } From 2ef028d08db70cb72f0af5f02495799d8074614b Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Thu, 19 Feb 2026 08:42:53 +0700 Subject: [PATCH 40/70] Update .env.example --- .env.example | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/.env.example b/.env.example index fd0ed07..5917573 100644 --- a/.env.example +++ b/.env.example @@ -1,23 +1,14 @@ -# ===== SAFE DEFAULT ===== -MODE=test -DRY_RUN=1 - -# Optional: protect wallet endpoints with API key -# API_KEY=your_secret_key - # ===== SOLANA ===== -# test mode recommended -SOL_RPC=https://api.devnet.solana.com - -# ===== EVM RPC ===== -# (execute is blocked by DRY_RUN, so still safe) -EVM_RPC_ETH=https://rpc.ankr.com/eth -EVM_RPC_BSC=https://rpc.ankr.com/bsc -EVM_RPC_BASE=https://mainnet.base.org - -# ===== 0x (optional) ===== -# ZEROX_API_KEY=your_0x_key - -# ===== GROQ AI (optional) ===== -GROQ_API_KEY=your_groq_key_here -GROQ_MODEL=llama-3.3-70b-versatile +SOL_PRIVATE_KEY= +SOL_RPC=https://api.mainnet-beta.solana.com +JUP_API_KEY= + +# ===== EVM ===== +EVM_PRIVATE_KEY=0x... +EVM_RPC=https://mainnet.base.org +EVM_CHAIN=base +OX_API_KEY= + +# ===== GROQ ===== +GROQ_API_KEY= +GROQ_MODEL=llama-3.1-8b-instant From 8e16711648ae63e97d1da78ed63bf73a9f5baa45 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Thu, 19 Feb 2026 08:43:48 +0700 Subject: [PATCH 41/70] Create logger.js --- src/core/logger.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/core/logger.js diff --git a/src/core/logger.js b/src/core/logger.js new file mode 100644 index 0000000..d91bab2 --- /dev/null +++ b/src/core/logger.js @@ -0,0 +1,9 @@ +export function step(msg) { + console.log(`\n=== ${msg} ===`); +} +export function info(msg) { + console.log(msg); +} +export function error(msg) { + console.error("❌", msg); +} From 79e31580c229d6d96ff0e9f917191b9bbea66ed6 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Thu, 19 Feb 2026 08:44:43 +0700 Subject: [PATCH 42/70] Create http.js --- src/core/http.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/core/http.js diff --git a/src/core/http.js b/src/core/http.js new file mode 100644 index 0000000..1d921f8 --- /dev/null +++ b/src/core/http.js @@ -0,0 +1,19 @@ +import { request } from "undici"; + +export async function httpJson(url, opts = {}) { + const res = await request(url, opts); + const text = await res.body.text(); + + let json; + try { + json = JSON.parse(text); + } catch { + throw new Error("Invalid JSON response"); + } + + if (res.statusCode >= 400) { + throw new Error(JSON.stringify(json)); + } + + return json; +} From 44a8f1d79d92665beb4bc40a1019d5c3de703d3d Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Thu, 19 Feb 2026 08:45:19 +0700 Subject: [PATCH 43/70] Create utils.js --- src/core/utils.js | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/core/utils.js diff --git a/src/core/utils.js b/src/core/utils.js new file mode 100644 index 0000000..bca699f --- /dev/null +++ b/src/core/utils.js @@ -0,0 +1,6 @@ +import bs58 from "bs58"; + +export function parseSolKey(pk) { + if (pk.startsWith("[")) return Uint8Array.from(JSON.parse(pk)); + return bs58.decode(pk); +} From ae24ca4ac7aeb498abfc1e68c9c1c887c696114c Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Thu, 19 Feb 2026 08:46:09 +0700 Subject: [PATCH 44/70] Create validation.js --- src/core/validation.js | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/core/validation.js diff --git a/src/core/validation.js b/src/core/validation.js new file mode 100644 index 0000000..3c75b5b --- /dev/null +++ b/src/core/validation.js @@ -0,0 +1,7 @@ +export function validateAmount(a) { + if (!a || Number(a) <= 0) throw new Error("Invalid amount"); +} + +export function validateSlippage(s) { + if (s > 500) throw new Error("Slippage too high"); +} From 02b5f52393e3fa481bf6a3a14b2bc30b1e8de5a6 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Thu, 19 Feb 2026 08:46:53 +0700 Subject: [PATCH 45/70] Create tokens.js --- rc/core/tokens.js | 1 + 1 file changed, 1 insertion(+) create mode 100644 rc/core/tokens.js diff --git a/rc/core/tokens.js b/rc/core/tokens.js new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/rc/core/tokens.js @@ -0,0 +1 @@ + From c3ae8e5da9abb742108e41494b12b2fd6c56c700 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Thu, 19 Feb 2026 08:47:29 +0700 Subject: [PATCH 46/70] Update tokens.js --- rc/core/tokens.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/rc/core/tokens.js b/rc/core/tokens.js index 8b13789..2b5c30e 100644 --- a/rc/core/tokens.js +++ b/rc/core/tokens.js @@ -1 +1,11 @@ +export const SOL = { + SOL: "So11111111111111111111111111111111111111112", + USDC: "EPjFWdd5AufqSSqeM2qG7G3h2Z4G6hYJrYd5b9z5xkz" +}; +export const EVM = { + base: { + USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + WETH: "0x4200000000000000000000000000000000000006" + } +}; From 3f6b4ec12e6f3271d937ff28f9428fb48f812d79 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Thu, 19 Feb 2026 08:47:50 +0700 Subject: [PATCH 47/70] Delete rc/core/tokens.js --- rc/core/tokens.js | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 rc/core/tokens.js diff --git a/rc/core/tokens.js b/rc/core/tokens.js deleted file mode 100644 index 2b5c30e..0000000 --- a/rc/core/tokens.js +++ /dev/null @@ -1,11 +0,0 @@ -export const SOL = { - SOL: "So11111111111111111111111111111111111111112", - USDC: "EPjFWdd5AufqSSqeM2qG7G3h2Z4G6hYJrYd5b9z5xkz" -}; - -export const EVM = { - base: { - USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", - WETH: "0x4200000000000000000000000000000000000006" - } -}; From 38d53c8947d6e83a5176b66324b2e713274a5913 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Thu, 19 Feb 2026 08:48:18 +0700 Subject: [PATCH 48/70] Create tokens.js --- src/core/tokens.js | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/core/tokens.js diff --git a/src/core/tokens.js b/src/core/tokens.js new file mode 100644 index 0000000..2b5c30e --- /dev/null +++ b/src/core/tokens.js @@ -0,0 +1,11 @@ +export const SOL = { + SOL: "So11111111111111111111111111111111111111112", + USDC: "EPjFWdd5AufqSSqeM2qG7G3h2Z4G6hYJrYd5b9z5xkz" +}; + +export const EVM = { + base: { + USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + WETH: "0x4200000000000000000000000000000000000006" + } +}; From 3451c09549194f1fb7a15ff4e1f2a417a930b110 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Thu, 19 Feb 2026 08:48:49 +0700 Subject: [PATCH 49/70] Create groq.js --- src/core/groq.js | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/core/groq.js diff --git a/src/core/groq.js b/src/core/groq.js new file mode 100644 index 0000000..82a7a47 --- /dev/null +++ b/src/core/groq.js @@ -0,0 +1,33 @@ +import { request } from "undici"; +import dotenv from "dotenv"; + +dotenv.config(); + +export async function groqParse(prompt) { + if (!process.env.GROQ_API_KEY) return null; + + try { + const res = await request("https://api.groq.com/openai/v1/chat/completions", { + method: "POST", + headers: { + Authorization: `Bearer ${process.env.GROQ_API_KEY}`, + "Content-Type": "application/json" + }, + body: JSON.stringify({ + model: process.env.GROQ_MODEL, + messages: [ + { + role: "system", + content: "Return ONLY JSON swap params" + }, + { role: "user", content: prompt } + ] + }) + }); + + const data = await res.body.json(); + return JSON.parse(data.choices[0].message.content); + } catch { + return null; + } +} From 41c3fa06ce0f8f5a988485f99604094b2cdaa830 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Thu, 19 Feb 2026 08:49:17 +0700 Subject: [PATCH 50/70] Create scout.js --- src/agents/scout.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/agents/scout.js diff --git a/src/agents/scout.js b/src/agents/scout.js new file mode 100644 index 0000000..7c8c9c5 --- /dev/null +++ b/src/agents/scout.js @@ -0,0 +1,24 @@ +import { step } from "../core/logger.js"; +import { groqParse } from "../core/groq.js"; + +function fallback(p) { + const amt = p.match(/\d+/)?.[0]; + return { + chain: "sol", + tokenIn: "USDC", + tokenOut: "SOL", + amount: amt, + slippageBps: 50 + }; +} + +export async function agentScout(input) { + step("SCOUT"); + + if (input.prompt) { + const ai = await groqParse(input.prompt); + return ai || fallback(input.prompt); + } + + return input; +} From 5d504eb5c1d8e68090003b9bfcc35fcf2cc2a5eb Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Thu, 19 Feb 2026 08:49:52 +0700 Subject: [PATCH 51/70] Create analyst.js --- src/agents/analyst.js | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/agents/analyst.js diff --git a/src/agents/analyst.js b/src/agents/analyst.js new file mode 100644 index 0000000..f54d35f --- /dev/null +++ b/src/agents/analyst.js @@ -0,0 +1,11 @@ +import { step } from "../core/logger.js"; + +export function agentAnalyst(input) { + step("ANALYST"); + + if (!input.chain) { + input.chain = input.tokenIn?.startsWith("0x") ? "base" : "sol"; + } + + return input; +} From 9008538c08f46d7b798e4bc3eb2b8c41926f3787 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Thu, 19 Feb 2026 08:50:19 +0700 Subject: [PATCH 52/70] Create riskgate.js --- src/agents/riskgate.js | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/agents/riskgate.js diff --git a/src/agents/riskgate.js b/src/agents/riskgate.js new file mode 100644 index 0000000..b69c131 --- /dev/null +++ b/src/agents/riskgate.js @@ -0,0 +1,11 @@ +import { step } from "../core/logger.js"; +import { validateAmount, validateSlippage } from "../core/validation.js"; + +export function agentRiskGate(input) { + step("RISK"); + + validateAmount(input.amount); + validateSlippage(input.slippageBps); + + return input; +} From 3cf1f1d614e3c38db7e755b5658086141d778a0a Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Thu, 19 Feb 2026 08:50:51 +0700 Subject: [PATCH 53/70] Create solana.js --- src/adapters/solana.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/adapters/solana.js diff --git a/src/adapters/solana.js b/src/adapters/solana.js new file mode 100644 index 0000000..5b6b927 --- /dev/null +++ b/src/adapters/solana.js @@ -0,0 +1,28 @@ +import { Connection, Keypair, VersionedTransaction } from "@solana/web3.js"; +import { httpJson } from "../core/http.js"; +import { parseSolKey } from "../core/utils.js"; +import dotenv from "dotenv"; + +dotenv.config(); + +export async function solSwap(input) { + const wallet = Keypair.fromSecretKey(parseSolKey(process.env.SOL_PRIVATE_KEY)); + const conn = new Connection(process.env.SOL_RPC); + + const quote = await httpJson(`https://api.jup.ag/swap/v1/quote?inputMint=${input.tokenIn}&outputMint=${input.tokenOut}&amount=${input.amount}&slippageBps=${input.slippageBps}`); + + const swap = await httpJson("https://api.jup.ag/swap/v1/swap", { + method: "POST", + body: JSON.stringify({ + quoteResponse: quote, + userPublicKey: wallet.publicKey.toString() + }), + headers: { "Content-Type": "application/json" } + }); + + const tx = VersionedTransaction.deserialize(Buffer.from(swap.swapTransaction, "base64")); + tx.sign([wallet]); + + const sig = await conn.sendRawTransaction(tx.serialize()); + return { txid: sig, quote }; +} From 282d1fec025ce2e704167290b7dadc76e49e1725 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Thu, 19 Feb 2026 08:51:24 +0700 Subject: [PATCH 54/70] Create evm.js --- src/adapters/evm.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/adapters/evm.js diff --git a/src/adapters/evm.js b/src/adapters/evm.js new file mode 100644 index 0000000..de3115e --- /dev/null +++ b/src/adapters/evm.js @@ -0,0 +1,20 @@ +import { ethers } from "ethers"; +import { httpJson } from "../core/http.js"; +import dotenv from "dotenv"; + +dotenv.config(); + +export async function evmSwap(input) { + const provider = new ethers.JsonRpcProvider(process.env.EVM_RPC); + const wallet = new ethers.Wallet(process.env.EVM_PRIVATE_KEY, provider); + + const quote = await httpJson(`https://api.0x.org/swap/v1/quote?sellToken=${input.tokenIn}&buyToken=${input.tokenOut}&sellAmount=${ethers.parseUnits(input.amount, 6)}`); + + const tx = await wallet.sendTransaction({ + to: quote.to, + data: quote.data, + value: BigInt(quote.value || 0) + }); + + return { txid: tx.hash, quote }; +} From b87ae6f416115a20c8ce1c0b3612a8676ba64189 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Thu, 19 Feb 2026 08:51:54 +0700 Subject: [PATCH 55/70] Create executor.js --- src/agents/executor.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/agents/executor.js diff --git a/src/agents/executor.js b/src/agents/executor.js new file mode 100644 index 0000000..970c471 --- /dev/null +++ b/src/agents/executor.js @@ -0,0 +1,14 @@ +import { step } from "../core/logger.js"; +import { solSwap } from "../adapters/solana.js"; +import { evmSwap } from "../adapters/evm.js"; + +export async function agentExecutor(input, opts = {}) { + step("EXECUTOR"); + + if (opts.dryRun) return { txid: null, quote: {} }; + + if (input.chain === "sol") return await solSwap(input); + if (input.chain === "base") return await evmSwap(input); + + throw new Error("Unsupported chain"); +} From c4046f8001eac35ffcf700956901b0290d4b5913 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Thu, 19 Feb 2026 08:52:21 +0700 Subject: [PATCH 56/70] Create index.js --- src/cli/index.js | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/cli/index.js diff --git a/src/cli/index.js b/src/cli/index.js new file mode 100644 index 0000000..ab1ae3b --- /dev/null +++ b/src/cli/index.js @@ -0,0 +1,36 @@ +import yargs from "yargs"; +import dotenv from "dotenv"; + +import { agentScout } from "../agents/scout.js"; +import { agentAnalyst } from "../agents/analyst.js"; +import { agentRiskGate } from "../agents/riskgate.js"; +import { agentExecutor } from "../agents/executor.js"; + +dotenv.config(); + +async function run(input, opts = {}) { + try { + const s = await agentScout(input); + const a = agentAnalyst(s); + const r = agentRiskGate(a); + const ex = await agentExecutor(r, opts); + + console.log(JSON.stringify({ + ...r, + txid: ex.txid, + status: "success" + }, null, 2)); + + } catch (e) { + console.error("❌", e.message); + } +} + +yargs(process.argv.slice(2)) +.command("swap", "execute", y=>y, argv=>{ + run(argv); +}) +.command("agent", "ai", y=>y, argv=>{ + run({prompt:argv.prompt}); +}) +.parse(); From e674d700b17a0ecc717453b95e914dd83b6e429f Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Thu, 19 Feb 2026 09:22:54 +0700 Subject: [PATCH 57/70] Update index.js --- src/cli/index.js | 171 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 155 insertions(+), 16 deletions(-) diff --git a/src/cli/index.js b/src/cli/index.js index ab1ae3b..b64fa3e 100644 --- a/src/cli/index.js +++ b/src/cli/index.js @@ -1,4 +1,7 @@ -import yargs from "yargs"; +import readline from "readline"; +import chalk from "chalk"; +import ora from "ora"; +import boxen from "boxen"; import dotenv from "dotenv"; import { agentScout } from "../agents/scout.js"; @@ -8,29 +11,165 @@ import { agentExecutor } from "../agents/executor.js"; dotenv.config(); -async function run(input, opts = {}) { +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +const ask = (q) => new Promise(res => rl.question(q, res)); + +// ================= HEADER ================= + +function header() { + console.clear(); + console.log( + boxen( + chalk.greenBright(` +INTERCOM SWAP BY PAK EKO 🚀 + `), + { + padding: 1, + borderColor: "green", + borderStyle: "round" + } + ) + ); +} + +// ================= MENU ================= + +function menuUI() { + console.log(chalk.cyan(` +1. Quote (Preview) +2. Swap (Execute) +3. Agent (AI) +4. Exit +`)); +} + +// ================= PIPELINE ================= + +async function runPipeline(input, opts = {}) { + const spinner = ora("Running pipeline...").start(); + try { const s = await agentScout(input); const a = agentAnalyst(s); const r = agentRiskGate(a); + + spinner.text = "Executing..."; + const ex = await agentExecutor(r, opts); - console.log(JSON.stringify({ - ...r, - txid: ex.txid, - status: "success" - }, null, 2)); + spinner.succeed("Done ✅"); + + console.log( + boxen( + chalk.greenBright(JSON.stringify({ + ...r, + txid: ex.txid, + status: "success" + }, null, 2)), + { padding: 1, borderColor: "green" } + ) + ); + + return ex; } catch (e) { - console.error("❌", e.message); + spinner.fail("Error ❌"); + console.log(chalk.red(e.message)); } } -yargs(process.argv.slice(2)) -.command("swap", "execute", y=>y, argv=>{ - run(argv); -}) -.command("agent", "ai", y=>y, argv=>{ - run({prompt:argv.prompt}); -}) -.parse(); +// ================= MENU FLOW ================= + +async function menu() { + header(); + menuUI(); + + const choice = await ask(chalk.yellow("Pilih menu: ")); + + // ===== QUOTE ===== + if (choice === "1") { + const tokenIn = await ask("Token In: "); + const tokenOut = await ask("Token Out: "); + const amount = await ask("Amount: "); + + console.log(chalk.blue("\n📊 Getting quote...\n")); + + await runPipeline({ + chain: "sol", + tokenIn, + tokenOut, + amount, + slippageBps: 50 + }, { dryRun: true }); + + return back(); + } + + // ===== SWAP ===== + if (choice === "2") { + const tokenIn = await ask("Token In: "); + const tokenOut = await ask("Token Out: "); + const amount = await ask("Amount: "); + + console.log(chalk.blue("\n📊 Preview first...\n")); + + await runPipeline({ + chain: "sol", + tokenIn, + tokenOut, + amount, + slippageBps: 50 + }, { dryRun: true }); + + const confirm = await ask(chalk.red("Execute swap? (y/n): ")); + + if (confirm.toLowerCase() === "y") { + console.log(chalk.green("\n💸 Executing real swap...\n")); + + await runPipeline({ + chain: "sol", + tokenIn, + tokenOut, + amount, + slippageBps: 50 + }); + } else { + console.log(chalk.gray("Cancelled.")); + } + + return back(); + } + + // ===== AGENT ===== + if (choice === "3") { + const prompt = await ask("AI Command: "); + + await runPipeline({ prompt }); + + return back(); + } + + // ===== EXIT ===== + if (choice === "4") { + console.log(chalk.green("Bye 🚀")); + rl.close(); + process.exit(0); + } + + console.log(chalk.red("Invalid choice")); + return back(); +} + +// ================= NAV ================= + +async function back() { + await ask(chalk.gray("\nEnter untuk kembali...")); + return menu(); +} + +// START +menu(); From f881a54cbf3d256d45b74584021055602c255b71 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Thu, 19 Feb 2026 09:34:54 +0700 Subject: [PATCH 58/70] Create dexscreener.js --- src/core/dexscreener.js | 89 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/core/dexscreener.js diff --git a/src/core/dexscreener.js b/src/core/dexscreener.js new file mode 100644 index 0000000..0cb1754 --- /dev/null +++ b/src/core/dexscreener.js @@ -0,0 +1,89 @@ +import { httpJson } from "./http.js"; + +const DEX_BASE = "https://api.dexscreener.com/latest/dex/tokens"; + +/** + * Dexscreener chains that are commonly used in pair objects: + * - Solana: "solana" + * - Base: "base" + */ +export function toDexChain(chain) { + const c = (chain || "").toLowerCase(); + if (c === "sol") return "solana"; + if (c === "base") return "base"; + return c; +} + +/** + * Fetch all pairs for a token address (CA / mint / 0x address). + * Returns array of pair objects (can be empty). + */ +export async function fetchDexTokenPairs(tokenAddress) { + if (!tokenAddress) throw new Error("Dexscreener: token address missing"); + const url = `${DEX_BASE}/${encodeURIComponent(tokenAddress.trim())}`; + const data = await httpJson(url, { method: "GET" }); + return Array.isArray(data?.pairs) ? data.pairs : []; +} + +/** + * Pick best pair for a chain (highest liquidity USD), + * optionally prefer a quote symbol like USDC/USDT/SOL/WETH. + */ +export function pickBestPair(pairs, { chain, preferQuoteSymbols = ["USDC", "USDT", "SOL", "WETH", "ETH"] } = {}) { + const dexChain = toDexChain(chain); + const filtered = pairs.filter((p) => (p.chainId || "").toLowerCase() === dexChain); + + if (!filtered.length) return null; + + // prefer quote tokens (e.g. USDC) if available + const preferred = filtered + .filter((p) => preferQuoteSymbols.includes((p.quoteToken?.symbol || "").toUpperCase())) + .sort((a, b) => (b.liquidity?.usd || 0) - (a.liquidity?.usd || 0)); + + if (preferred.length) return preferred[0]; + + // fallback: max liquidity + return filtered.sort((a, b) => (b.liquidity?.usd || 0) - (a.liquidity?.usd || 0))[0]; +} + +/** + * Normalize a dex pair into a compact "market snapshot" for risk engine. + */ +export function normalizePair(pair) { + if (!pair) return null; + + const createdAt = pair.pairCreatedAt ? Number(pair.pairCreatedAt) : null; // ms epoch + const ageMs = createdAt ? Date.now() - createdAt : null; + + const snap = { + dex: pair.dexId || null, + chainId: pair.chainId || null, + pairAddress: pair.pairAddress || null, + + base: { + symbol: pair.baseToken?.symbol || null, + name: pair.baseToken?.name || null, + address: pair.baseToken?.address || null + }, + quote: { + symbol: pair.quoteToken?.symbol || null, + name: pair.quoteToken?.name || null, + address: pair.quoteToken?.address || null + }, + + priceUsd: pair.priceUsd ? Number(pair.priceUsd) : null, + liquidityUsd: pair.liquidity?.usd ? Number(pair.liquidity.usd) : 0, + fdvUsd: pair.fdv ? Number(pair.fdv) : null, + + volume24hUsd: pair.volume?.h24 ? Number(pair.volume.h24) : 0, + buys24h: pair.txns?.h24?.buys ? Number(pair.txns.h24.buys) : 0, + sells24h: pair.txns?.h24?.sells ? Number(pair.txns.h24.sells) : 0, + + priceChange24hPct: pair.priceChange?.h24 ? Number(pair.priceChange.h24) : null, + + createdAt, + ageMs + }; + + return snap; +} From e0aa343e496d6d6ee54920c606047f0b150d2563 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Thu, 19 Feb 2026 09:36:05 +0700 Subject: [PATCH 59/70] Update analyst.js --- src/agents/analyst.js | 65 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/src/agents/analyst.js b/src/agents/analyst.js index f54d35f..d3c832e 100644 --- a/src/agents/analyst.js +++ b/src/agents/analyst.js @@ -1,11 +1,66 @@ -import { step } from "../core/logger.js"; +import { step, info } from "../core/logger.js"; +import { isEvmAddressLike, isSolanaMintLike } from "../core/tokens.js"; +import { fetchDexTokenPairs, pickBestPair, normalizePair } from "../core/dexscreener.js"; -export function agentAnalyst(input) { +/** + * Analyst: + * - Auto-detect chain from CA + * - Fetch Dexscreener market snapshot (best pair by liquidity) + * - Attach market info to input as `market` (for riskgate & final output) + */ +export async function agentAnalyst(input) { step("ANALYST"); - if (!input.chain) { - input.chain = input.tokenIn?.startsWith("0x") ? "base" : "sol"; + if (!input.tokenIn || !input.tokenOut) { + throw new Error("Token missing (tokenIn/tokenOut)"); } - return input; + const tokenIn = String(input.tokenIn).trim(); + const tokenOut = String(input.tokenOut).trim(); + + // auto chain detection if not provided + let chain = input.chain ? String(input.chain).toLowerCase() : null; + if (!chain) { + if (isEvmAddressLike(tokenIn) || isEvmAddressLike(tokenOut)) chain = "base"; + else if (isSolanaMintLike(tokenIn) || isSolanaMintLike(tokenOut)) chain = "sol"; + else chain = "sol"; + } + + const out = { ...input, chain, tokenIn, tokenOut }; + + // Pull Dexscreener snapshot IF tokenOut looks like CA/mint (or tokenIn), helpful for risk analysis + // We try tokenOut first (the thing you are buying), then tokenIn fallback. + const probeAddr = isEvmAddressLike(tokenOut) || isSolanaMintLike(tokenOut) + ? tokenOut + : (isEvmAddressLike(tokenIn) || isSolanaMintLike(tokenIn) ? tokenIn : null); + + if (!probeAddr) { + info("Analyst: token not a CA/mint -> skip Dexscreener snapshot (symbol mode)"); + return out; + } + + try { + info(`Analyst: fetching Dexscreener pairs for ${probeAddr} ...`); + const pairs = await fetchDexTokenPairs(probeAddr); + const best = pickBestPair(pairs, { chain }); + const snap = normalizePair(best); + + out.market = { + source: "dexscreener", + tokenAddress: probeAddr, + bestPair: snap + }; + + if (snap) { + info(`Analyst: best pair ${snap.dex} liquidity=$${Math.round(snap.liquidityUsd)} 24hVol=$${Math.round(snap.volume24hUsd)}`); + } else { + info("Analyst: no matching pair found on Dexscreener for this chain"); + } + + return out; + } catch (e) { + // Don't hard-fail if Dexscreener down; riskgate can still run basic checks + info(`Analyst: Dexscreener fetch failed (non-fatal): ${e.message}`); + return out; + } } From 53b7f772d411a44b90569e32fa8e018cbb5dac2b Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Thu, 19 Feb 2026 09:37:21 +0700 Subject: [PATCH 60/70] Update riskgate.js --- src/agents/riskgate.js | 132 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 130 insertions(+), 2 deletions(-) diff --git a/src/agents/riskgate.js b/src/agents/riskgate.js index b69c131..662cb98 100644 --- a/src/agents/riskgate.js +++ b/src/agents/riskgate.js @@ -1,11 +1,139 @@ -import { step } from "../core/logger.js"; +import { step, info } from "../core/logger.js"; import { validateAmount, validateSlippage } from "../core/validation.js"; +function now() { + return Date.now(); +} + +function fmtAge(ms) { + if (ms == null) return "unknown"; + const s = Math.floor(ms / 1000); + if (s < 60) return `${s}s`; + const m = Math.floor(s / 60); + if (m < 60) return `${m}m`; + const h = Math.floor(m / 60); + if (h < 48) return `${h}h`; + const d = Math.floor(h / 24); + return `${d}d`; +} + +/** + * RiskGate: + * - Basic checks: amount/slippage, chain supported + * - Dexscreener heuristic risk scoring: + * - liquidity too low + * - token too new + * - 24h volume too low + * - sells=0 (potential honeypot indicator; not definitive) + * - insane price change (pump/dump) + * + * Output: + * - attaches `risk` object: score, level, flags + * - can BLOCK if too risky (configurable) + */ export function agentRiskGate(input) { step("RISK"); validateAmount(input.amount); validateSlippage(input.slippageBps); - return input; + const chain = (input.chain || "").toLowerCase(); + if (!["sol", "base"].includes(chain)) { + throw new Error(`Unsupported chain: ${input.chain} (supported: sol, base)`); + } + + // ---- configurable thresholds (env optional) ---- + const MIN_LIQ_USD = Number(process.env.RISK_MIN_LIQ_USD || "15000"); // $15k + const MIN_VOL24_USD = Number(process.env.RISK_MIN_VOL24_USD || "5000"); // $5k + const MIN_AGE_MIN = Number(process.env.RISK_MIN_AGE_MIN || "60"); // 60 minutes + const MAX_PUMP_PCT = Number(process.env.RISK_MAX_PUMP_PCT || "250"); // 250% 24h change + const BLOCK_SCORE = Number(process.env.RISK_BLOCK_SCORE || "75"); // >=75 block + + const flags = []; + let score = 0; + + // ---- market snapshot from analyst (optional) ---- + const snap = input.market?.bestPair || null; + + if (!snap) { + flags.push("NO_MARKET_SNAPSHOT"); + score += 10; // mild + } else { + // liquidity + if (snap.liquidityUsd < MIN_LIQ_USD) { + flags.push(`LOW_LIQUIDITY_$${Math.round(snap.liquidityUsd)}`); + score += 35; + } + + // age + const ageMin = snap.ageMs != null ? Math.floor(snap.ageMs / 60000) : null; + if (ageMin != null && ageMin < MIN_AGE_MIN) { + flags.push(`TOO_NEW_${fmtAge(snap.ageMs)}`); + score += 25; + } + + // volume + if (snap.volume24hUsd < MIN_VOL24_USD) { + flags.push(`LOW_VOLUME24H_$${Math.round(snap.volume24hUsd)}`); + score += 20; + } + + // suspicious sells + if (snap.buys24h > 0 && snap.sells24h === 0) { + flags.push("NO_SELLS_24H (possible honeypot/illiquid)"); + score += 30; + } + + // huge pump/dump + if (snap.priceChange24hPct != null && Math.abs(snap.priceChange24hPct) > MAX_PUMP_PCT) { + flags.push(`EXTREME_PRICE_CHANGE_24H_${snap.priceChange24hPct}%`); + score += 25; + } + } + + // slippage safety + if (Number(input.slippageBps) > 200) { + flags.push("HIGH_SLIPPAGE_BPS"); + score += 15; + } + + // Normalize score + if (score > 100) score = 100; + + const level = score >= BLOCK_SCORE ? "BLOCK" : (score >= 45 ? "CAUTION" : "SAFE"); + + const risk = { + score, + level, + flags, + thresholds: { + MIN_LIQ_USD, + MIN_VOL24_USD, + MIN_AGE_MIN, + MAX_PUMP_PCT, + BLOCK_SCORE + }, + marketSummary: snap + ? { + dex: snap.dex, + pair: snap.pairAddress, + liquidityUsd: snap.liquidityUsd, + volume24hUsd: snap.volume24hUsd, + buys24h: snap.buys24h, + sells24h: snap.sells24h, + age: fmtAge(snap.ageMs), + priceChange24hPct: snap.priceChange24hPct + } + : null + }; + + info(`RiskGate: level=${risk.level} score=${risk.score}`); + if (risk.flags.length) info(`Flags: ${risk.flags.join(" | ")}`); + + // Default: block risky token BEFORE execute + if (risk.level === "BLOCK" && !process.env.RISK_ALLOW_BLOCKED) { + throw new Error(`RiskGate BLOCKED swap: score=${risk.score} flags=${risk.flags.join(", ")}`); + } + + return { ...input, chain, risk }; } From 548d8f4fab03903fd09e35dfa30dd3abe5d82af0 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Thu, 19 Feb 2026 09:39:44 +0700 Subject: [PATCH 61/70] Update index.js --- src/cli/index.js | 169 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 128 insertions(+), 41 deletions(-) diff --git a/src/cli/index.js b/src/cli/index.js index b64fa3e..d9b29f0 100644 --- a/src/cli/index.js +++ b/src/cli/index.js @@ -16,7 +16,7 @@ const rl = readline.createInterface({ output: process.stdout }); -const ask = (q) => new Promise(res => rl.question(q, res)); +const ask = (q) => new Promise((res) => rl.question(q, res)); // ================= HEADER ================= @@ -39,12 +39,57 @@ INTERCOM SWAP BY PAK EKO 🚀 // ================= MENU ================= function menuUI() { - console.log(chalk.cyan(` + console.log( + chalk.cyan(` 1. Quote (Preview) 2. Swap (Execute) 3. Agent (AI) 4. Exit -`)); +`) + ); +} + +// ================= HELPERS ================= + +function renderMarket(r) { + const snap = r?.market?.bestPair; + if (!snap) { + return chalk.gray("Market: (no Dexscreener snapshot)\n"); + } + + const age = snap.ageMs != null ? `${Math.floor(snap.ageMs / 60000)}m` : "unknown"; + return ( + chalk.whiteBright("Market snapshot (Dexscreener)\n") + + `- Chain: ${snap.chainId}\n` + + `- DEX: ${snap.dex}\n` + + `- Pair: ${snap.pairAddress}\n` + + `- PriceUsd: ${snap.priceUsd ?? "n/a"}\n` + + `- LiquidityUsd: $${Math.round(snap.liquidityUsd || 0)}\n` + + `- Volume24hUsd: $${Math.round(snap.volume24hUsd || 0)}\n` + + `- Buys/Sells 24h: ${snap.buys24h || 0}/${snap.sells24h || 0}\n` + + `- Age: ${age}\n` + ); +} + +function renderRisk(r) { + const risk = r?.risk; + if (!risk) return chalk.gray("Risk: (no risk object)\n"); + + const color = + risk.level === "SAFE" + ? chalk.green + : risk.level === "CAUTION" + ? chalk.yellow + : chalk.red; + + const flags = risk.flags?.length ? risk.flags.join(" | ") : "none"; + + return ( + chalk.whiteBright("RiskGate\n") + + `- Level: ${color(risk.level)}\n` + + `- Score: ${color(String(risk.score))}\n` + + `- Flags: ${flags}\n` + ); } // ================= PIPELINE ================= @@ -53,32 +98,63 @@ async function runPipeline(input, opts = {}) { const spinner = ora("Running pipeline...").start(); try { + // Scout can be async (Groq) const s = await agentScout(input); - const a = agentAnalyst(s); - const r = agentRiskGate(a); - spinner.text = "Executing..."; + // ✅ Analyst is async now (Dexscreener fetch) + spinner.text = "Analyst: fetching market data..."; + const a = await agentAnalyst(s); - const ex = await agentExecutor(r, opts); + spinner.text = "RiskGate: evaluating..."; + const r = agentRiskGate(a); - spinner.succeed("Done ✅"); + spinner.stop(); + // Show pre-execution intelligence console.log( boxen( - chalk.greenBright(JSON.stringify({ - ...r, - txid: ex.txid, - status: "success" - }, null, 2)), - { padding: 1, borderColor: "green" } + chalk.whiteBright( + `Plan\n- chain: ${r.chain}\n- in: ${r.tokenIn}\n- out: ${r.tokenOut}\n- amount: ${r.amount}\n- slippageBps: ${r.slippageBps}\n\n` + + renderMarket(r) + + "\n" + + renderRisk(r) + ), + { padding: 1, borderColor: r?.risk?.level === "BLOCK" ? "red" : "green" } ) ); - return ex; + // Re-start spinner for execution + const execSpin = ora(opts.dryRun ? "Quote mode..." : "Executing swap on-chain...").start(); + + const ex = await agentExecutor(r, opts); + + execSpin.succeed(opts.dryRun ? "Quote OK ✅" : "Execute OK ✅"); + + const summary = { + chain: r.chain, + mode: opts.dryRun ? "dry-run" : "execute", + tokenIn: r.tokenIn, + tokenOut: r.tokenOut, + amountIn: String(r.amount), + slippageBps: Number(r.slippageBps), + quote: ex.quote || null, + txid: ex.txid || null, + risk: r.risk || null, + status: "success" + }; + + console.log( + boxen(chalk.greenBright(JSON.stringify(summary, null, 2)), { + padding: 1, + borderColor: "green" + }) + ); + return ex; } catch (e) { spinner.fail("Error ❌"); - console.log(chalk.red(e.message)); + console.log(chalk.red(e?.message || String(e))); + return null; } } @@ -92,50 +168,61 @@ async function menu() { // ===== QUOTE ===== if (choice === "1") { - const tokenIn = await ask("Token In: "); - const tokenOut = await ask("Token Out: "); - const amount = await ask("Amount: "); + const tokenIn = await ask("Token In (symbol/mint/CA): "); + const tokenOut = await ask("Token Out (symbol/mint/CA): "); + const amount = await ask("Amount (human, ex: 1 / 0.1): "); + const sl = await ask("Slippage bps (default 50): "); + const slippageBps = sl?.trim() ? Number(sl.trim()) : 50; console.log(chalk.blue("\n📊 Getting quote...\n")); - await runPipeline({ - chain: "sol", - tokenIn, - tokenOut, - amount, - slippageBps: 50 - }, { dryRun: true }); + await runPipeline( + { + chain: "sol", + tokenIn, + tokenOut, + amount, + slippageBps + }, + { dryRun: true } + ); return back(); } // ===== SWAP ===== if (choice === "2") { - const tokenIn = await ask("Token In: "); - const tokenOut = await ask("Token Out: "); - const amount = await ask("Amount: "); + const chain = (await ask("Chain (sol/base) [default sol]: ")).trim() || "sol"; + const tokenIn = await ask("Token In (symbol/mint/CA): "); + const tokenOut = await ask("Token Out (symbol/mint/CA): "); + const amount = await ask("Amount (human, ex: 1 / 0.1): "); + const sl = await ask("Slippage bps (default 50): "); + const slippageBps = sl?.trim() ? Number(sl.trim()) : 50; - console.log(chalk.blue("\n📊 Preview first...\n")); + console.log(chalk.blue("\n📊 Preview first (dry-run)...\n")); - await runPipeline({ - chain: "sol", - tokenIn, - tokenOut, - amount, - slippageBps: 50 - }, { dryRun: true }); + await runPipeline( + { + chain, + tokenIn, + tokenOut, + amount, + slippageBps + }, + { dryRun: true } + ); - const confirm = await ask(chalk.red("Execute swap? (y/n): ")); + const confirm = await ask(chalk.red("Execute swap REAL MAINNET? (y/n): ")); if (confirm.toLowerCase() === "y") { console.log(chalk.green("\n💸 Executing real swap...\n")); await runPipeline({ - chain: "sol", + chain, tokenIn, tokenOut, amount, - slippageBps: 50 + slippageBps }); } else { console.log(chalk.gray("Cancelled.")); @@ -146,7 +233,7 @@ async function menu() { // ===== AGENT ===== if (choice === "3") { - const prompt = await ask("AI Command: "); + const prompt = await ask("AI Command (boleh pakai CA): "); await runPipeline({ prompt }); From a4b7c825483487633d3c2a419e0675a08a4909f4 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Thu, 19 Feb 2026 09:43:39 +0700 Subject: [PATCH 62/70] Update tokens.js --- src/core/tokens.js | 49 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/src/core/tokens.js b/src/core/tokens.js index 2b5c30e..16ecd07 100644 --- a/src/core/tokens.js +++ b/src/core/tokens.js @@ -1,11 +1,54 @@ -export const SOL = { +// Token resolver: accept SYMBOL or ADDRESS/CA +// - Solana: mint base58 32..44 chars +// - EVM: 0x address + +export const SOLANA_MINTS = { SOL: "So11111111111111111111111111111111111111112", - USDC: "EPjFWdd5AufqSSqeM2qG7G3h2Z4G6hYJrYd5b9z5xkz" + // USDC mainnet + USDC: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + // USDT mainnet + USDT: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB" }; -export const EVM = { +export const EVM_TOKENS = { base: { + // Base mainnet USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", WETH: "0x4200000000000000000000000000000000000006" } }; + +export function isSolanaMintLike(s) { + return typeof s === "string" && /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(s.trim()); +} + +export function isEvmAddressLike(s) { + return typeof s === "string" && /^0x[0-9a-fA-F]{40}$/.test(s.trim()); +} + +export function resolveSolanaMint(symOrAddr) { + if (!symOrAddr) throw new Error("Missing token"); + const v = symOrAddr.trim(); + + if (isSolanaMintLike(v)) return v; + + const k = v.toUpperCase(); + if (SOLANA_MINTS[k]) return SOLANA_MINTS[k]; + + throw new Error(`Unsupported SOL token/mint: ${symOrAddr}`); +} + +export function resolveEvmToken(chain, symOrAddr) { + if (!symOrAddr) throw new Error("Missing token"); + const v = symOrAddr.trim(); + + if (isEvmAddressLike(v)) return v; + + const map = EVM_TOKENS[chain]; + if (!map) throw new Error(`Unsupported EVM chain: ${chain}`); + + const k = v.toUpperCase(); + if (map[k]) return map[k]; + + throw new Error(`Unsupported EVM token: ${symOrAddr} (chain=${chain})`); +} From 8503aea587e6a55244fdca336d351949c80667a9 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Thu, 19 Feb 2026 09:44:15 +0700 Subject: [PATCH 63/70] Update analyst.js --- src/agents/analyst.js | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/src/agents/analyst.js b/src/agents/analyst.js index d3c832e..7faee8f 100644 --- a/src/agents/analyst.js +++ b/src/agents/analyst.js @@ -2,12 +2,6 @@ import { step, info } from "../core/logger.js"; import { isEvmAddressLike, isSolanaMintLike } from "../core/tokens.js"; import { fetchDexTokenPairs, pickBestPair, normalizePair } from "../core/dexscreener.js"; -/** - * Analyst: - * - Auto-detect chain from CA - * - Fetch Dexscreener market snapshot (best pair by liquidity) - * - Attach market info to input as `market` (for riskgate & final output) - */ export async function agentAnalyst(input) { step("ANALYST"); @@ -18,7 +12,7 @@ export async function agentAnalyst(input) { const tokenIn = String(input.tokenIn).trim(); const tokenOut = String(input.tokenOut).trim(); - // auto chain detection if not provided + // auto chain detect if not provided let chain = input.chain ? String(input.chain).toLowerCase() : null; if (!chain) { if (isEvmAddressLike(tokenIn) || isEvmAddressLike(tokenOut)) chain = "base"; @@ -28,19 +22,19 @@ export async function agentAnalyst(input) { const out = { ...input, chain, tokenIn, tokenOut }; - // Pull Dexscreener snapshot IF tokenOut looks like CA/mint (or tokenIn), helpful for risk analysis - // We try tokenOut first (the thing you are buying), then tokenIn fallback. - const probeAddr = isEvmAddressLike(tokenOut) || isSolanaMintLike(tokenOut) - ? tokenOut - : (isEvmAddressLike(tokenIn) || isSolanaMintLike(tokenIn) ? tokenIn : null); + // Dexscreener only makes sense if user provided a CA/mint/0x + const probeAddr = + isEvmAddressLike(tokenOut) || isSolanaMintLike(tokenOut) + ? tokenOut + : (isEvmAddressLike(tokenIn) || isSolanaMintLike(tokenIn) ? tokenIn : null); if (!probeAddr) { - info("Analyst: token not a CA/mint -> skip Dexscreener snapshot (symbol mode)"); + info("Analyst: symbol mode (no CA) -> skip Dexscreener snapshot"); return out; } try { - info(`Analyst: fetching Dexscreener pairs for ${probeAddr} ...`); + info(`Analyst: Dexscreener lookup for ${probeAddr} ...`); const pairs = await fetchDexTokenPairs(probeAddr); const best = pickBestPair(pairs, { chain }); const snap = normalizePair(best); @@ -52,15 +46,14 @@ export async function agentAnalyst(input) { }; if (snap) { - info(`Analyst: best pair ${snap.dex} liquidity=$${Math.round(snap.liquidityUsd)} 24hVol=$${Math.round(snap.volume24hUsd)}`); + info(`Analyst: bestPair dex=${snap.dex} liq=$${Math.round(snap.liquidityUsd || 0)} vol24=$${Math.round(snap.volume24hUsd || 0)}`); } else { - info("Analyst: no matching pair found on Dexscreener for this chain"); + info("Analyst: no chain-matching pair found on Dexscreener"); } return out; } catch (e) { - // Don't hard-fail if Dexscreener down; riskgate can still run basic checks - info(`Analyst: Dexscreener fetch failed (non-fatal): ${e.message}`); + info(`Analyst: Dexscreener failed (non-fatal): ${e.message}`); return out; } } From a8def63c8f2a3dd6a0dfaf04ee4003ae71043c50 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Thu, 19 Feb 2026 09:49:34 +0700 Subject: [PATCH 64/70] Update agentScout.js --- src/agents/agentScout.js | 197 +++++++++++++++++++++++++++++---------- 1 file changed, 149 insertions(+), 48 deletions(-) diff --git a/src/agents/agentScout.js b/src/agents/agentScout.js index 2539c5c..20f4259 100644 --- a/src/agents/agentScout.js +++ b/src/agents/agentScout.js @@ -1,54 +1,155 @@ -import { banner, sleep, fmt } from "./agentShared.js"; - -export async function agentScout({ quoteFn, input }) { - // input: { chain, tokenIn, tokenOut, amountIn, slippageBps } - console.log(banner("🤖 Agent 1: SCOUT (Planner)")); - console.log("Scout: menerima brief swap…"); - await sleep(250); - - console.log(`Scout: chain=${input.chain}`); - console.log(`Scout: tokenIn=${input.tokenIn}`); - console.log(`Scout: tokenOut=${input.tokenOut}`); - console.log(`Scout: amountIn=${input.amountIn}`); - console.log(`Scout: slippage=${input.slippageBps} bps`); - await sleep(250); - - console.log("Scout: fetching quote…"); - const q = await quoteFn(input); // expect { amountOut, minOut, path, warnings? } - await sleep(200); - - console.log("Scout: quote received ✅"); - console.log(`Scout: estOut = ${fmt(q.amountOut)}`); - console.log(`Scout: minOut = ${fmt(q.minOut)}`); - console.log(`Scout: path = ${q.path?.join(" -> ") || "-"}`); - - // Risk gate (basic + keliatan pro) - const warnings = []; - if (input.slippageBps > 300) warnings.push("Slippage > 3% (risky)"); - if (!q.path || q.path.length < 2) warnings.push("Route path invalid"); - if (q.warnings?.length) warnings.push(...q.warnings); - - console.log("\nScout: risk gate report:"); - if (!warnings.length) { - console.log("Scout: ✅ PASS (no warnings)"); - } else { - console.log("Scout: ⚠️ WARNINGS:"); - for (const w of warnings) console.log(`- ${w}`); +import { step, info } from "../core/logger.js"; +import { groqParse } from "../core/groq.js"; +import { isEvmAddressLike, isSolanaMintLike } from "../core/tokens.js"; + +// ---------- helpers ---------- +function normToken(t) { + if (!t) return null; + const v = String(t).trim(); + + // If CA/mint/address, keep as-is + if (isEvmAddressLike(v) || isSolanaMintLike(v)) return v; + + // Otherwise treat as symbol + return v.toUpperCase(); +} + +function clampSlippageBps(n) { + const x = Number(n); + if (!Number.isFinite(x)) return 50; + if (x < 1) return 1; + if (x > 500) return 500; + return Math.round(x); +} + +function parseSlippageBps(prompt) { + const p = prompt.toLowerCase(); + + // "slippage 0.5%" or "slip 1%" + const mPct = p.match(/slipp(?:age)?\s*([0-9]+(?:\.[0-9]+)?)\s*%/i); + if (mPct) return clampSlippageBps(Number(mPct[1]) * 100); + + // "slippage 50 bps" + const mBps = p.match(/slipp(?:age)?\s*([0-9]{1,4})\s*bps/i); + if (mBps) return clampSlippageBps(Number(mBps[1])); + + // "50 bps" anywhere + const mBps2 = p.match(/\b([0-9]{1,4})\s*bps\b/i); + if (mBps2) return clampSlippageBps(Number(mBps2[1])); + + return 50; +} + +function parseChain(prompt) { + const p = prompt.toLowerCase(); + if (p.includes(" base ")) return "base"; + if (p.includes("base")) return "base"; + if (p.includes("solana")) return "sol"; + if (p.includes(" sol ")) return "sol"; + if (p.includes("sol ")) return "sol"; + return null; +} + +function extractAmount(prompt) { + // first number in prompt + const m = prompt.match(/([0-9]+(?:\.[0-9]+)?)/); + return m ? m[1] : null; +} + +function extractTokens(prompt) { + // Support: "swap to/ke " + // token can be: symbol (USDC) or CA/mint (base58 32..44) or 0x... + const p = prompt.trim(); + + // This regex tries to capture tokenIn and tokenOut around "to/ke" + const re = /swap\s+[0-9]+(?:\.[0-9]+)?\s+([A-Za-z0-9_.:-]{2,}|0x[0-9a-fA-F]{40}|[1-9A-HJ-NP-Za-km-z]{32,44})\s+(?:to|ke)\s+([A-Za-z0-9_.:-]{2,}|0x[0-9a-fA-F]{40}|[1-9A-HJ-NP-Za-km-z]{32,44})/i; + const m = p.match(re); + if (m) return { tokenIn: m[1], tokenOut: m[2] }; + + // Fallback: scan for CA/mint/0x addresses + const evm = p.match(/0x[0-9a-fA-F]{40}/g) || []; + const sol = p.match(/[1-9A-HJ-NP-Za-km-z]{32,44}/g) || []; + const addrs = [...evm, ...sol]; + + if (addrs.length >= 2) return { tokenIn: addrs[0], tokenOut: addrs[1] }; + + // Fallback: scan for symbols and use first two symbols that aren't words + const words = p + .replace(/[%(),]/g, " ") + .split(/\s+/) + .filter(Boolean); + + // remove common keywords + const stop = new Set(["swap", "to", "ke", "slippage", "slip", "on", "chain", "di"]); + const syms = words.filter((w) => !stop.has(w.toLowerCase()) && !/^[0-9]+(\.[0-9]+)?$/.test(w)); + + if (syms.length >= 2) return { tokenIn: syms[0], tokenOut: syms[1] }; + + return { tokenIn: null, tokenOut: null }; +} + +function finalizePlan(plan, prompt) { + const tokenIn = normToken(plan.tokenIn); + const tokenOut = normToken(plan.tokenOut); + + // if chain not set, detect from tokens or prompt + let chain = plan.chain ? String(plan.chain).toLowerCase() : null; + if (!chain) chain = parseChain(prompt); + if (!chain) { + if (isEvmAddressLike(tokenIn) || isEvmAddressLike(tokenOut)) chain = "base"; + else chain = "sol"; } - // Plan object buat handoff ke Executor - const plan = { - chain: input.chain, - tokenIn: input.tokenIn, - tokenOut: input.tokenOut, - amountIn: input.amountIn, - slippageBps: input.slippageBps, - amountOut: q.amountOut, - minOut: q.minOut, - path: q.path, - warnings, + const amount = plan.amount ? String(plan.amount) : extractAmount(prompt); + const slippageBps = clampSlippageBps(plan.slippageBps ?? parseSlippageBps(prompt)); + + return { + chain, + tokenIn, + tokenOut, + amount, + slippageBps, + // keep original prompt for audit/debug (safe) + prompt }; +} + +// ---------- main ---------- +export async function agentScout(input) { + step("SCOUT"); + + // CLI path (already structured) + if (!input?.prompt) { + info(`Scout: CLI params mode`); + return input; + } + + const prompt = String(input.prompt || "").trim(); + if (!prompt) throw new Error("Empty prompt for agent mode"); + + // 1) Try Groq (if available) - must return JSON + const ai = await groqParse(prompt); + if (ai && typeof ai === "object") { + const plan = finalizePlan(ai, prompt); + info(`Scout(Groq): chain=${plan.chain}, in=${plan.tokenIn}, out=${plan.tokenOut}, amount=${plan.amount}, slippageBps=${plan.slippageBps}`); + return plan; + } + + // 2) Fallback parser (robust regex) + const chain = parseChain(prompt); + const amount = extractAmount(prompt); + const { tokenIn, tokenOut } = extractTokens(prompt); + const slippageBps = parseSlippageBps(prompt); + + const plan = finalizePlan({ chain, tokenIn, tokenOut, amount, slippageBps }, prompt); + info(`Scout(Fallback): chain=${plan.chain}, in=${plan.tokenIn}, out=${plan.tokenOut}, amount=${plan.amount}, slippageBps=${plan.slippageBps}`); + + // Hard fail if still missing core fields + if (!plan.tokenIn || !plan.tokenOut || !plan.amount) { + throw new Error( + `Scout failed to parse prompt. Try format: "swap 1 USDC to SOL slippage 0.5%" or use CA directly.` + ); + } - console.log("\nScout: handoff plan → Executor ✅"); return plan; } From 2583adfe60583b1053fee78f4ebdeee9cd7efa98 Mon Sep 17 00:00:00 2001 From: pakeko78 Date: Thu, 19 Feb 2026 10:05:16 +0700 Subject: [PATCH 65/70] Update README.md --- README.md | 1130 ++++++++--------------------------------------------- 1 file changed, 165 insertions(+), 965 deletions(-) diff --git a/README.md b/README.md index 3bedecb..3b21fa4 100644 --- a/README.md +++ b/README.md @@ -1,1068 +1,268 @@ -# Intercom Swap - -This repo is a fork of upstream **Intercom** (Trac-Systems/intercom): a reference implementation of the Intercom stack on Trac Network for an internet of agents. - -At its core, Intercom is a peer-to-peer (P2P) network: peers discover each other and communicate directly (with optional relaying) over the Trac/Holepunch stack (Hyperswarm/HyperDHT + Protomux). There is no central server required for sidechannel messaging. - -This fork adds a non-custodial swap harness: - -- Negotiate via **request-for-quote (RFQ)** messages over **Intercom sidechannels** (P2P). -- Settle **BTC over Lightning** <> **USDT on Solana** using a shared Solana escrow program (HTLC-style). - -Links: -- Upstream Intercom: `https://github.com/Trac-Systems/intercom` -- This fork: `https://github.com/TracSystems/intercom-swap` - -## Architecture (High-Level) -Intercom Swap is a local-first P2P system with one core runtime and multiple optional control/settlement paths. - -```text - Humans + Autonomous Agents - | - +--------------+--------------+ - | | - Structured control Natural language - (UI + tool calls) (optional prompting) - | | - +--------------+--------------+ - v - Intercom runtime peer - (identity + local state store) - | - +-----------------+-------------------+ - | | - v v - P2P coordination fabric Optional app extension - - Sidechannels (RFQ + swap) - Local-first contracts/features - - Subnet replication - Trac Network tx path (TNK gas) - | - +--------------+--------------+ - | | - v v - Lightning settlement Solana settlement - (BTC leg) (USDT leg) -``` +# 🧠 INTERCOM SWAP BY PAK EKO 🚀 -Key idea: -- Intercom handles coordination and agent communication. -- Settlement happens on Lightning + Solana. -- Contract usage on Trac Network is optional and extensible. +CLI-based **AI Multi-Agent Swap Engine** +🔥 REAL swap on MAINNET (Solana) +🔥 Dexscreener-powered analysis +🔥 Risk engine + Agent pipeline --- -## What Intercom Is - -Intercom is a Trac stack for autonomous agents: -- **Sidechannels**: fast, ephemeral P2P messaging (Hyperswarm + Noise). -- **Features**: integrate non-agent services/tools into the same network. -- **Contracts (optional)**: deterministic state + optional chat. -- **MSB (optional)**: value-settled transactions. - -This fork keeps Intercom intact and layers swap + ops tooling on top. - ---- +## 📍 TRAC ADDRESS -## Table Of Contents - -- [Run Strategy Matrix](#run-strategy-matrix) -- [Install And Operate From `SKILL.md`](#install-and-operate-from-skillmd) -- [How To Use `SKILL.md` With An Agent](#how-to-use-skillmd-with-an-agent) -- [Conceptual Flow (BTC(LN) <> USDT(Solana))](#conceptual-flow-btcln--usdtsolana) -- [External APIs / RPCs (Defaults)](#external-apis--rpcs-defaults) -- [Command Surface (Scripts = "Function Calls")](#command-surface-scripts--function-calls) -- [Start Intercom Peers (`run-swap-*`)](#start-intercom-peers-run-swap-) -- [SC-Bridge Control (`swapctl`)](#sc-bridge-control-swapctl) -- [RFQ Bots (`rfq-maker` / `rfq-taker`)](#rfq-bots-rfq-maker--rfq-taker) -- [Recovery (`swaprecover`)](#recovery-swaprecover) -- [Solana Wallet Tooling (`solctl`)](#solana-wallet-tooling-solctl) -- [Solana Escrow Program Tooling (`escrowctl`)](#solana-escrow-program-tooling-escrowctl) -- [Lightning Operator Tooling (`lnctl`)](#lightning-operator-tooling-lnctl) -- [Optional LND Local Lifecycle (`lndctl` / `lndpw`)](#optional-lnd-local-lifecycle-lndctl--lndpw) -- [Prompt Router (Optional)](#prompt-router-optional) -- [Tests (Mandatory)](#tests-mandatory) -- [Secrets + Repo Hygiene](#secrets--repo-hygiene) +``` +trac1jwh8vrc50x7r8ysfx0v7d2k2qlkv999zkjl5w6sg7rktmmf6qhysp3w76d +``` --- -## Run Strategy Matrix - -Choose one path before running commands. Do not mix paths in a single instance. +## ⚡ OVERVIEW -| Goal | Path | Typical Network | Data Isolation Rule | -|---|---|---|---| -| Validate code and workflows | Test path | LN regtest + Solana local/devnet | test stores + test receipts DB + test promptd port | -| Upgrade an existing deployment | Upgrade path | same as current deployment | keep backup, preserve current stores, rerun tests | -| Operate with real funds | Mainnet path | LN mainnet + Solana mainnet | separate mainnet stores/receipts/ports; never reuse test data | -| Human-first operation | Collin path | any | Collin talks to one promptd instance at a time | -| Agent-first automation | Headless path | any | prefer deterministic scripts/tool calls over free-form prompting | +INTERCOM SWAP BY PAK EKO is a CLI tool designed for: -Minimal rule set: -- Always decide `test` vs `mainnet` first. -- Keep test and mainnet fully separated (store names, DB paths, ports, audit dirs). -- For mainnet, use public DHT bootstraps (local DHT is test-only). -- Run tests before first live settlement. +- 💱 Real on-chain token swaps (MAINNET) +- 🤖 Multi-agent execution pipeline: + - Scout → intent parsing + - Analyst → market analysis (Dexscreener) + - RiskGate → safety checks + - Executor → on-chain swap +- 📊 Real-time token analysis +- 🛡️ Built-in risk scoring system --- -## Install And Operate From `SKILL.md` +## 🧠 AGENT PIPELINE -`SKILL.md` is the canonical **installer + runbook** for this repo. If you are an agent, treat it as the source of truth for: -- installation steps -- runtime requirements (Pear, Node) -- first-run decisions (sidechannels, invites, PoW) -- operations (LN/Solana, recovery, tests) - -### Recommended Models (For Install/Upgrades) - -Installation and large merges are easiest with a top-tier coding model. +``` +USER INPUT + ↓ +Scout (parse intent) + ↓ +Analyst (Dexscreener data) + ↓ +RiskGate (liquidity, volume, age, etc) + ↓ +Executor (Jupiter swap → MAINNET) +``` -Recommended: -- OpenAI: **GPT-5.3+** (Codex, `xhigh`) -- Anthropic: **Claude Opus 4.6+** +--- -OpenClaw can use and control this stack autonomously (install/upgrade via `SKILL.md`, ops via scripts and optional `promptd` tool calls, including backend worker tools `intercomswap_tradeauto_*`). +## 🚀 FEATURES -Local/open-weight models can work too, but use a high-grade one. +- ✅ CLI UI (Pro Max Interactive) +- ✅ Real swap via Jupiter Aggregator +- ✅ Token CA / Mint support +- ✅ Dexscreener integration +- ✅ Risk scoring engine +- ✅ Dry-run preview before execution +- ✅ AI-style agent workflow --- -## How To Use `SKILL.md` With An Agent +## 📦 INSTALLATION -Example prompts (copy/paste): - -1. Install -```text -Install this repo using SKILL.md. Run all tests (unit + e2e). Report what you ran and any failures. +### 1. Clone repository +```bash +git clone https://github.com/pakeko78/intercom-swap-by-pakeko +cd intercom-swap-by-pakeko ``` -2. Install + staging tests -```text -Install this repo using SKILL.md. Run unit + local e2e. Then run a smoke test on test networks (LN regtest + Solana devnet) if supported. Report results. +### 2. Install dependencies +```bash +npm install ``` -2b. Decide and execute one run path first -```text -Read SKILL.md and pick exactly one run path (test / upgrade / mainnet / collin / headless). Explain why that path matches the goal, then execute it end-to-end. -``` +--- -3. Update workflow -```text -Pull the latest version of this fork, resolve merge conflicts, and run all tests (unit + e2e). If testnet smoke tests exist, run them too. Only then proceed to mainnet checks. -``` +## ⚙️ ENV SETUP -3b. Switch to mainnet (fresh instance) -```text -Create a clean mainnet instance: do NOT reuse any test stores or receipts DBs. Wipe/rotate test data only, keep mainnet keys separate. Then bring up mainnet peer + promptd + Collin and run a mainnet readiness checklist (funding + LN channel ready + Solana RPC reachable). +```bash +cp .env.example .env +nano .env ``` -4. Mainnet start -```text -Install this repo using SKILL.md, run all tests (unit + e2e), then run the mainnet bring-up checklist and start maker+taker peers on mainnet (with user-provided Solana RPC + Solana keypairs + LN node configuration). Report the exact commands run and any failures. -``` +Fill this: -5. Enable Collin prompting (LLM mode) -```text -Enable LLM prompting for Collin. Tell me exactly what config you need (OpenAI-compatible base_url, api_key or token file, model, max_tokens, temperature) and where it must be stored (gitignored). Validate by running a prompt that posts an Offer and confirm it appears in the UI. ``` - -6. Operator support mode -```text -I’m operating Collin and I’m stuck: “”. Explain what it means and the exact next click/command to fix it. Do not guess; inspect the repo and logs. +SOL_PRIVATE_KEY=YOUR_PRIVATE_KEY +SOL_RPC=https://api.mainnet-beta.solana.com ``` --- -## Conceptual Flow (BTC(LN) <> USDT(Solana)) - -```text -Rendezvous sidechannel(s) (any; examples: 0000intercom, 0000intercomswapbtcusdt, my-swap-room) - | - | swap.svc_announce (service + offers[]) [periodic rebroadcast; sidechannels have no history] - | Offer (optional) -> RFQ (manual or backend-auto-from-offer) -> QUOTE -> QUOTE_ACCEPT - | - pre-filter by app_hash + fee caps + refund window - v -per-trade invite-only swap: - | - | TERMS (binding: fees, mint, refund_after_unix, ...) - | ACCEPT - | LN_INVOICE (payment_hash) - | SOL_ESCROW_CREATED (escrow PDA + vault ATA) - v -Settlement (BTC over Lightning <> USDT on Solana) - 1) Maker creates + posts LN invoice (receiver inbound liquidity check must pass) - 2) Taker runs LN route precheck and posts `ln_route_precheck_ok` (swap.status) - 3) Maker escrows USDT (Solana) only after taker precheck is OK - 4) Taker verifies escrow on-chain (hard rule: no escrow, no pay) - 5) Taker pays LN invoice -> learns preimage - 6) Taker claims USDT on Solana using preimage - 7) Refund path after sol_refund_after_unix if LN payment never happens -``` - -## External APIs / RPCs (Defaults) +## 🔐 PRIVATE KEY FORMAT -This stack touches a few external endpoints. Defaults are chosen so local e2e is easy, and live ops are configurable: +Supported formats: -- Price oracle (HTTP): by default uses public exchange APIs (no keys): `binance,coinbase,gate,kucoin,okx,bitstamp,kraken`. - - Enabled on peers via `--price-oracle 1` (included in `scripts/run-swap-*.sh`). - - Configure providers via `--price-providers ""`. -- Solana (JSON-RPC over HTTP): bots/tools default to local validator `http://127.0.0.1:8899`. - - Configure via `--solana-rpc-url ""` (comma-separated failover pool). -- Bitcoin/LN: the BTC leg is **Lightning** (CLN or LND). - - Local e2e uses docker regtest stacks under `dev/` (includes `bitcoind`). - - Mainnet uses your local LN node (CLN via `bitcoind` RPC, or LND via `neutrino` or `bitcoind` backend). - - This repo does not require a separate public Bitcoin explorer API by default. +- ✅ Base58 string +- ✅ JSON array (Solana format) -If any of your HTTP/RPC endpoints require auth headers (Bearer/API tokens), see **Authenticated API Endpoints** near the end of this README. +⚠️ SECURITY WARNING: +- NEVER use your main wallet +- Always use burner wallet --- -## Command Surface (Scripts = "Function Calls") - -After installation, day-to-day operation should be done by invoking scripts (macOS/Linux `.sh`, Windows `.ps1`). The `.mjs` files are the canonical CLIs; wrappers exist to keep invocation stable and tool-call friendly. - -### Script Index - -| Area | macOS/Linux | Windows | Canonical | Purpose | -|---|---|---|---|---| -| Bootstrap | `scripts/bootstrap.sh` | n/a | bash | Install Pear runtime + deps | -| Start peer (maker/service) | `scripts/run-swap-maker.sh` | `scripts/run-swap-maker.ps1` | shell | Start a peer with SC-Bridge + price oracle and join an RFQ channel | -| Start peer (taker/client) | `scripts/run-swap-taker.sh` | `scripts/run-swap-taker.ps1` | shell | Start a peer with SC-Bridge + price oracle and join an RFQ channel; pins `SWAP_INVITER_KEYS` for `swap:*` | -| Peer lifecycle supervisor | `scripts/peermgr.sh` | `scripts/peermgr.ps1` | `scripts/peermgr.mjs` | Start/stop/restart background peers (headless) without keeping a terminal open | -| SC-Bridge control | `scripts/swapctl.sh` | `scripts/swapctl.ps1` | `scripts/swapctl.mjs` | Sidechannel ops + signed message helpers | -| SC-Bridge control (token auto) | `scripts/swapctl-peer.sh` | `scripts/swapctl-peer.ps1` | wrapper | Same as `swapctl`, but reads token from `onchain/sc-bridge/.token` | -| RFQ maker bot | `scripts/rfq-maker-peer.sh` | `scripts/rfq-maker-peer.ps1` | `scripts/rfq-maker.mjs` | Quote RFQs; optionally run full swap state machine | -| RFQ taker bot | `scripts/rfq-taker-peer.sh` | `scripts/rfq-taker-peer.ps1` | `scripts/rfq-taker.mjs` | Send RFQ; accept quote; optionally run full swap state machine | -| RFQ bot control | `scripts/rfqbotmgr.sh` | `scripts/rfqbotmgr.ps1` | `scripts/rfqbotmgr.mjs` | Start/stop/restart RFQ bot instances without stopping the peer | -| Recovery | `scripts/swaprecover.sh` | `scripts/swaprecover.ps1` | `scripts/swaprecover.mjs` | List/show receipts; claim/refund escrows | -| Solana wallet ops | `scripts/solctl.sh` | `scripts/solctl.ps1` | `scripts/solctl.mjs` | Keypairs, balances, ATA, token transfers | -| Solana escrow ops | `scripts/escrowctl.sh` | `scripts/escrowctl.ps1` | `scripts/escrowctl.mjs` | Program config, fee vaults, escrow inspection | -| Solana program ops (maintainers) | `scripts/solprogctl.sh` | `scripts/solprogctl.ps1` | `scripts/solprogctl.mjs` | Build/deploy the Solana program | -| Lightning ops | `scripts/lnctl.sh` | `scripts/lnctl.ps1` | `scripts/lnctl.mjs` | Addresses, channels, invoices, payments | -| LND local lifecycle (optional) | `scripts/lndctl.sh` | `scripts/lndctl.ps1` | `scripts/lndctl.mjs` | Generate `lnd.conf`, start/stop, create/unlock wallet | -| LND password helper (optional) | `scripts/lndpw.sh` | `scripts/lndpw.ps1` | shell | Write an LND wallet password file (no trailing newline) | - ---- - -### Start Intercom Peers (`run-swap-*`) - -| Function call | What it does | Parameters | -|---|---|---| -| `scripts/run-swap-maker.sh [storeName] [scBridgePort] [rfqChannel] [...extra peer flags]` | Starts a maker/service peer, enables SC-Bridge + price oracle, joins the RFQ channel | Positional args; optional env: `SIDECHANNEL_POW` (default `1`), `SIDECHANNEL_POW_DIFFICULTY` (default `12`) | -| `SWAP_INVITER_KEYS="" scripts/run-swap-taker.sh [storeName] [scBridgePort] [rfqChannel] [...extra peer flags]` | Starts a taker/client peer and pins inviter key(s) for `swap:*` invite-only channels | Requires `SWAP_INVITER_KEYS`; same optional env vars as maker | - -Notes: -| Item | Details | -|---|---| -| Token files | Created under `onchain/sc-bridge/.token` (gitignored). | -| RFQ channel | Any sidechannel works. Many operators use a dedicated rendezvous (example: `0000intercomswapbtcusdt`) to reduce noise, but `0000intercom` works too. | -| Subnet channel | Keep `--subnet-channel` consistent across peers (mismatches can prevent connections). | +## ▶️ RUN CLI ---- - -### Peer Lifecycle Supervisor (`peermgr`) - -`peermgr` is a local supervisor for starting/stopping `pear run` peers in the background (so you don’t need to keep a terminal open). - -Notes: -- It enforces: **never run the same peer store twice**. -- It stores state + logs under `onchain/peers/` (gitignored). -- It always starts the peer in **headless mode** (`--terminal 0`). +```bash +npm run cli +``` -#### Commands +or: -| Command | What it does | -|---|---| -| `scripts/peermgr.sh start --name --store --sc-port --sidechannels ` | Start a peer and join one or more extra sidechannels on startup | -| `scripts/peermgr.sh stop --name ` | Stop the peer process | -| `scripts/peermgr.sh restart --name ` | Restart using the last saved config | -| `scripts/peermgr.sh status [--name ]` | Show state + PID + liveness | +```bash +node src/cli/index.js +``` --- -### SC-Bridge Control (`swapctl`) - -`swapctl` is the SC-Bridge client CLI. It controls a **running peer** over WebSocket, and (when needed) signs locally using the peer keypair file (SC-Bridge never signs). - -#### Connection - -| Flag | Required | Meaning | -|---|---:|---| -| `--url ws://127.0.0.1:` | yes | SC-Bridge websocket URL | -| `--token ` | yes | SC-Bridge token (from `onchain/sc-bridge/.token`) | -| `--peer-keypair ` | signing only | Peer `keypair.json` (usually `stores//db/keypair.json`) for commands that create signed payloads | - -#### Token Convenience Wrapper (Recommended) - -| Wrapper | What it does | -|---|---| -| `scripts/swapctl-peer.sh ` | Reads `onchain/sc-bridge/.token` and calls `swapctl` with `--url/--token` | -| `scripts/swapctl-peer.ps1 ` | Same for Windows | - -#### Command Reference - -##### Introspection +## 🎮 CLI MENU -| Command | What it does | Important flags | -|---|---|---| -| `info` | Peer info (pubkey, joined channels, SC-Bridge status) | none | -| `stats` | Peer runtime stats | none | -| `price-get` | Price snapshot from the peer's price feature | none | -| `watch` | Stream messages for debugging/observability | `--channels `, `--kinds `, `--trade-id `, `--pretty 0/1`, `--raw 0/1` | - -##### Sidechannel I/O - -| Command | What it does | Flags | -|---|---|---| -| `join` | Join a sidechannel | `--channel `; optional: `--invite `, `--welcome ` | -| `leave` | Leave a sidechannel | `--channel ` | -| `open` | Request others to open a channel (via the entry channel) | `--channel --via `; optional: `--invite <...>`, `--welcome <...>` | -| `send` | Send plaintext or JSON to a channel | `--channel ` and one of: `--text ` or `--json `; optional: `--invite <...>`, `--welcome <...>` | - -##### Service Presence (Directory Beacon) - -| Command | What it does | Flags | -|---|---|---| -| `svc-announce` | Broadcast a signed service announcement | Required: `--channels --name