Bot & Automation
Market AI
/root/hermes-projects/Market AI
apps/api/src/server.ts
text
import cors from "@fastify/cors";
import helmet from "@fastify/helmet";
import Fastify from "fastify";
import {
BOT_NAME,
generateMemeSignalAnalysis,
logger,
memeSignalRequestSchema,
providerStatusSnapshot,
scannerCriteriaSchema,
tradeRequestSchema,
} from "@market-ai/core";
import { buildAiSignalReviewPrompt } from "@market-ai/prompts";
import { generateAiReview, isGithubModelsConfigured, memeMarketDataRouter, tradeExecutor } from "@market-ai/provider-adapters";
const port = Number(process.env.PORT ?? 8080);
const host = process.env.HOST ?? "0.0.0.0";
const app = Fastify({
logger: false,
genReqId: () => crypto.randomUUID(),
});
await app.register(helmet);
await app.register(cors, {
origin: process.env.APP_BASE_URL ? [process.env.APP_BASE_URL] : true,
});
app.get("/health", async () => ({
status: "ok",
service: "meme-market-ai-api",
bot: BOT_NAME,
tradingMode: process.env.TRADING_MODE === "live" ? "live" : "paper",
env: process.env.NODE_ENV ?? "development",
timestamp: new Date().toISOString(),
}));
app.get("/meme/scan", async (request, reply) => {
const parsed = scannerCriteriaSchema.safeParse(request.query);
if (!parsed.success) {
return reply.status(400).send({
error: {
code: "VALIDATION_ERROR",
message: "Scanner query is not valid.",
safeDetail: parsed.error.issues.map((issue) => issue.message).join(", "),
},
});
}
const candidates = await memeMarketDataRouter.scanMemeCoins(parsed.data);
return { candidates };
});
app.get("/meme/search", async (request, reply) => {
const query = request.query as { q?: string; chainId?: string };
if (!query.q?.trim()) {
return reply.status(400).send({
error: { code: "VALIDATION_ERROR", message: "Query parameter q is required." },
});
}
const candidates = await memeMarketDataRouter.searchMemeCoins(query.q, {
chainId: query.chainId ?? "solana",
minLiquidityUsd: 0,
minVolumeH24: 0,
minScore: 0,
maxAgeHours: 3650,
limit: 10,
});
return { candidates };
});
app.post("/meme/analyze", async (request, reply) => {
const parsed = memeSignalRequestSchema.safeParse(request.body);
if (!parsed.success) {
return reply.status(400).send({
error: {
code: "VALIDATION_ERROR",
message: "Analysis request is not valid.",
safeDetail: parsed.error.issues.map((issue) => issue.message).join(", "),
},
});
}
const candidate = await memeMarketDataRouter.getMemeCandidate(parsed.data.query, parsed.data.chainId ?? "solana");
const signal = generateMemeSignalAnalysis(parsed.data, candidate);
const aiReview = isGithubModelsConfigured()
? await generateAiReview({ prompt: buildAiSignalReviewPrompt(signal) }).catch((error: unknown) => {
const safeMessage = error instanceof Error ? error.message : "AI review unavailable.";
logger.error("ai_review_failed", { requestId: request.id, token: candidate.token.symbol, safeMessage });
return {
provider: "github_models" as const,
model: process.env.GITHUB_MODELS_MODEL,
content: "AI review belum tersedia. Gunakan signal teknikal hanya sebagai analisis edukatif dengan risk management ketat.",
};
})
: undefined;
return { signal, aiReview };
});
app.post("/trade/quote", async (request, reply) => {
const parsed = tradeRequestSchema.safeParse(request.body);
if (!parsed.success) {
return reply.status(400).send({
error: {
code: "VALIDATION_ERROR",
message: "Trade request is not valid.",
safeDetail: parsed.error.issues.map((issue) => issue.message).join(", "),
},
});
}
return { quote: await tradeExecutor.buildQuote(parsed.data) };
});
app.post("/trade/execute", async (request, reply) => {
const parsed = tradeRequestSchema.safeParse(request.body);
if (!parsed.success) {
return reply.status(400).send({
error: {
code: "VALIDATION_ERROR",
message: "Trade request is not valid.",
safeDetail: parsed.error.issues.map((issue) => issue.message).join(", "),
},
});
}
const quote = await tradeExecutor.buildQuote(parsed.data);
const result = await tradeExecutor.executeTrade(quote);
return { result };
});
app.get("/ai/status", async () => ({
provider: process.env.AI_PROVIDER ?? "not_configured",
githubModelsConfigured: isGithubModelsConfigured(),
model: process.env.GITHUB_MODELS_MODEL ?? null,
}));
app.get("/ai/smoke", async () => {
const result = await generateAiReview({
prompt: "Reply with OK only. This is a neutral API connectivity test.",
system: "You are a connectivity test assistant.",
});
return {
ok: result.content.toLowerCase().includes("ok"),
provider: result.provider,
model: result.model,
sample: result.content.slice(0, 80),
};
});
app.get("/providers/status", async () => ({
providers: providerStatusSnapshot(),
}));
app.get("/watchlist/default", async () => ({
items: ["BONK", "WIF", "PEPE"],
}));
app.get("/alerts/schema", async () => ({
supportedConditions: ["price_above", "price_below", "volume_spike", "score_above", "liquidity_below"],
example: { tokenAddress: "TOKEN_ADDRESS", condition: "score_above", target: 75 },
}));
app.setErrorHandler((error: Error & { code?: string }, request, reply) => {
logger.error("api_request_failed", {
requestId: request.id,
errorCode: error.code,
message: error.message,
});
return reply.status(500).send({
error: {
code: "INTERNAL_ERROR",
message: "Something went wrong.",
safeDetail: "Coba lagi nanti. Detail internal tidak ditampilkan untuk keamanan.",
},
});
});
try {
await app.listen({ port, host });
logger.info("api_started", { port, host });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown startup error";
logger.error("api_start_failed", { message });
process.exit(1);
}