Skip to content

Commit 6336b24

Browse files
committed
add stocks
1 parent 4521e45 commit 6336b24

2 files changed

Lines changed: 197 additions & 7 deletions

File tree

listen-interface/src/components/Chart.tsx

Lines changed: 176 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
UTCTimestamp,
99
} from "lightweight-charts";
1010
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
11+
import { z } from "zod";
1112
import { useListenMetadata } from "../hooks/useListenMetadata";
1213
import { CandlestickData, CandlestickDataSchema } from "../lib/types";
1314
import { useTokenStore } from "../store/tokenStore";
@@ -31,6 +32,34 @@ export interface ChartProps {
3132
// Available time intervals for the chart
3233
const INTERVALS = ["30s", "1m", "5m", "15m", "30m", "1h", "4h", "1d"] as const;
3334

35+
// GeckoTerminal API schemas
36+
const GeckoTerminalPoolSchema = z.object({
37+
data: z.array(
38+
z.object({
39+
attributes: z.object({
40+
address: z.string(),
41+
volume_usd: z.object({
42+
h24: z.string(),
43+
}),
44+
}),
45+
})
46+
),
47+
});
48+
49+
const GeckoTerminalOHLCVSchema = z.object({
50+
data: z.object({
51+
attributes: z.object({
52+
ohlcv_list: z.array(
53+
z.array(z.union([z.number(), z.string()]))
54+
),
55+
}),
56+
}),
57+
});
58+
59+
// Cache for pool addresses to prevent repeated API calls
60+
const poolAddressCache: Record<string, { address: string; timestamp: number }> = {};
61+
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
62+
3463
// Add TradingView color constants
3564
const TV_COLORS = {
3665
GREEN: "#26a69a",
@@ -39,6 +68,134 @@ const TV_COLORS = {
3968
RED_TRANSPARENT: "rgba(239, 83, 80, 0.3)",
4069
} as const;
4170

71+
// Find the most liquid pool for a token on Solana
72+
async function findSolanaPoolAddress(tokenAddress: string): Promise<string | null> {
73+
const cacheKey = `solana:${tokenAddress}`;
74+
const cached = poolAddressCache[cacheKey];
75+
76+
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
77+
return cached.address;
78+
}
79+
80+
try {
81+
const response = await fetch(
82+
`https://api.geckoterminal.com/api/v2/networks/solana/tokens/${tokenAddress}/pools`,
83+
{
84+
headers: {
85+
Accept: "application/json;version=20230302",
86+
},
87+
}
88+
);
89+
90+
if (!response.ok) {
91+
console.error(`Failed to fetch pools for token ${tokenAddress}`);
92+
return null;
93+
}
94+
95+
const json = await response.json();
96+
const result = GeckoTerminalPoolSchema.safeParse(json);
97+
98+
if (!result.success) {
99+
console.error("Failed to parse pool response:", result.error);
100+
return null;
101+
}
102+
103+
// Sort pools by 24h volume to get the most active pool
104+
const sortedPools = result.data.data.sort(
105+
(a, b) =>
106+
parseFloat(b.attributes.volume_usd.h24) -
107+
parseFloat(a.attributes.volume_usd.h24)
108+
);
109+
110+
if (sortedPools.length > 0) {
111+
const address = sortedPools[0].attributes.address;
112+
poolAddressCache[cacheKey] = {
113+
address,
114+
timestamp: Date.now(),
115+
};
116+
return address;
117+
}
118+
119+
return null;
120+
} catch (error) {
121+
console.error("Failed to fetch pool address:", error);
122+
return null;
123+
}
124+
}
125+
126+
// Map chart intervals to GeckoTerminal timeframes
127+
function mapIntervalToGeckoTimeframe(interval: string): { timeframe: string; aggregate: number } {
128+
switch (interval) {
129+
case "30s":
130+
case "1m":
131+
return { timeframe: "minute", aggregate: 1 };
132+
case "5m":
133+
return { timeframe: "minute", aggregate: 5 };
134+
case "15m":
135+
return { timeframe: "minute", aggregate: 15 };
136+
case "30m":
137+
return { timeframe: "hour", aggregate: 1 }; // Use 1h as closest available
138+
case "1h":
139+
return { timeframe: "hour", aggregate: 1 };
140+
case "4h":
141+
return { timeframe: "hour", aggregate: 4 };
142+
case "1d":
143+
return { timeframe: "day", aggregate: 1 };
144+
default:
145+
return { timeframe: "minute", aggregate: 1 };
146+
}
147+
}
148+
149+
// Fetch OHLC data from GeckoTerminal
150+
async function fetchGeckoTerminalOHLC(
151+
poolAddress: string,
152+
interval: string
153+
): Promise<CandlestickData | null> {
154+
try {
155+
const { timeframe, aggregate } = mapIntervalToGeckoTimeframe(interval);
156+
157+
const response = await fetch(
158+
`https://api.geckoterminal.com/api/v2/networks/solana/pools/${poolAddress}/ohlcv/${timeframe}?aggregate=${aggregate}&limit=1000`,
159+
{
160+
headers: {
161+
Accept: "application/json;version=20230302",
162+
},
163+
}
164+
);
165+
166+
if (!response.ok) {
167+
console.error(`Failed to fetch OHLC data for pool ${poolAddress}`);
168+
return null;
169+
}
170+
171+
const json = await response.json();
172+
const result = GeckoTerminalOHLCVSchema.safeParse(json);
173+
174+
if (!result.success) {
175+
console.error("Failed to parse OHLC response:", result.error);
176+
return null;
177+
}
178+
179+
// Convert GeckoTerminal OHLCV format to our Candlestick format
180+
const candlesticks: CandlestickData = result.data.data.attributes.ohlcv_list.map((item) => {
181+
const [timestamp, open, high, low, close, volume] = item;
182+
return {
183+
timestamp: typeof timestamp === "number" ? timestamp : parseInt(timestamp as string),
184+
open: typeof open === "number" ? open : parseFloat(open as string),
185+
high: typeof high === "number" ? high : parseFloat(high as string),
186+
low: typeof low === "number" ? low : parseFloat(low as string),
187+
close: typeof close === "number" ? close : parseFloat(close as string),
188+
volume: typeof volume === "number" ? volume : parseFloat(volume as string),
189+
};
190+
});
191+
192+
return candlesticks;
193+
} catch (error) {
194+
console.error("Failed to fetch GeckoTerminal OHLC data:", error);
195+
return null;
196+
}
197+
}
198+
42199
export function InnerChart({ data, displayOhlc }: InnerChartProps) {
43200
const chartContainerRef = useRef<HTMLDivElement>(null);
44201
const chartRef = useRef<ReturnType<typeof createChart>>(null);
@@ -446,14 +603,27 @@ export function Chart({ mint, interval: defaultInterval = "30s" }: ChartProps) {
446603
if (isDisposed.current) return;
447604

448605
try {
449-
const response = await fetch(
450-
// use prod for charts always
451-
`https://api.listen-rs.com/v1/adapter/candlesticks?mint=${mint}&interval=${selectedInterval}`
452-
);
453-
const responseData = CandlestickDataSchema.parse(await response.json());
606+
// First try to get data from GeckoTerminal
607+
const poolAddress = await findSolanaPoolAddress(mint);
608+
let responseData: CandlestickData | null = null;
609+
610+
if (poolAddress) {
611+
console.log(`Found pool address ${poolAddress} for token ${mint}, fetching from GeckoTerminal`);
612+
responseData = await fetchGeckoTerminalOHLC(poolAddress, selectedInterval);
613+
}
614+
615+
// If GeckoTerminal fails or no pool found, fall back to custom API
616+
if (!responseData || responseData.length === 0) {
617+
console.log(`Falling back to custom API for token ${mint}`);
618+
const response = await fetch(
619+
// use prod for charts always
620+
`https://api.listen-rs.com/v1/adapter/candlesticks?mint=${mint}&interval=${selectedInterval}`
621+
);
622+
responseData = CandlestickDataSchema.parse(await response.json());
623+
}
454624

455625
if (!isDisposed.current) {
456-
setData(responseData);
626+
setData(responseData || []);
457627
setIsLoading(false);
458628
}
459629
} catch (error) {

listen-interface/src/prompts/system.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export function systemPrompt(
1919
defaultAmount: string,
2020
isGuest: boolean,
2121
currentSolanaPrice?: TokenPrice,
22-
hyperliquidPortfolio?: HyperliquidPortfolioOverview | null
22+
hyperliquidPortfolio?: HyperliquidPortfolioOverview | null,
2323
): string {
2424
const hasWallet = pubkey !== null && pubkey !== "";
2525
const hasEvmWallet = address !== null && address !== "";
@@ -34,6 +34,7 @@ export function systemPrompt(
3434
prompt += `## Listen Token\n${listenToken}\n\n`;
3535
prompt += `## Handling Errors\n${handlingErrors}\n\n`;
3636
prompt += `## Hyperliquid\n${hyperliquid}\n\n`;
37+
prompt += `## Stocks\n${stocks}\n\n`;
3738

3839
if (!hasWallet || isGuest) {
3940
prompt += `## Onboarding Required (Solana)\n${onboarding(hasWallet, isGuest)}\n\n`;
@@ -72,3 +73,22 @@ const currentTimeUnderline = `While your training data has a cutoff date that ha
7273
const listenToken = `The app users are interacting with you in is called Listen, created and maintained by @piotreksol who loves AI dearly and quit his job at Google to develop your capabilities full-time. It has a token $listen (also referred to as listen-rs, address: Cn5Ne1vmR9ctMGY9z5NC71A3NYFvopjXNyxYtfVYpump) that was born after tokenization of the github.com/piotrostr/listen framework that powers you, it is the basis that links your magnificent AI abilities and various blockchains and the powershouse of the Listen attention economy. It provides the holders fee discounts and generous usage tiers. Be sure to express that if the users ask.`;
7374

7475
const hyperliquid = `You have access to Hyperliquid, which allows you to trade BTC, HYPE (the hyperliquid token) and perpetuals on all of the major symbols.`;
76+
77+
const stocks = `There are tokenized stocks available on Solana. You can suggest to the user to potentially diversify a bit into those stocks. The tickers available:
78+
- CRCL (Circle): XsueG8BtpquVJX9LVLLEGuViXUungE6WmK5YZ3p3bd1
79+
- NVDA (NVIDIA): Xsc9qvGR1efVDFGLrVsmkzv3qi45LTBjeUKSPmx9qEh
80+
- TSLA (Tesla): XsDoVfqeBukxuZHWhdvWHBhgEHjGNst4MLodqsJHzoB
81+
- MSTR (MicroStrategy): XsP7xzNPvEHS1m6qfanPUGjNmdnmsLKEoNAnHjdxxyZ
82+
- GOOGL (Alphabet): XsCPL9dNWBMvFtTmwcCA5v3xWPSMEBCszbQdiLLq6aN
83+
- AAPL (Apple): XsbEhLAtcf6HdfpFZ5xEMdqW8nfAvcsP5bdudRLJzJp
84+
- HOOD (Robinhood): XsvNBAYkrDRNhA7wPHQfX3ZUXZyZLdnCQDfHZ56bzpg
85+
- AMZN (Amazon): Xs3eBt7uRfJX8QUs4suhyU8p2M6DoUDrJyWBa8LLZsg
86+
- COIN (Coinbase): Xs7ZdzSHLU9ftNJsii5fCeJhoRWSC32SQGzGQtePxNu
87+
- META (Meta Platforms): Xsa62P5mvPszXL1krVUnU5ar38bBSVcWAB6fmPCo5Zu
88+
89+
Indices:
90+
- SPY (S&P 500 ETF): XsoCS1TfEyfFhfvj8EtZ528L3CaKBDBRqRapnBbDF2W
91+
- QQQ (Nasdaq-100): Xs8S1uUs1zvS2p7iwtsG3b6fkhpvmwz4GYU3gWAmWHZ
92+
93+
Those work the same way as any other SPL token.
94+
`;

0 commit comments

Comments
 (0)