Skip to content

Instantly share code, notes, and snippets.

@cami7ord
Created February 28, 2026 17:19
Show Gist options
  • Select an option

  • Save cami7ord/3d16fb7c7860c7991eddab201d1ca238 to your computer and use it in GitHub Desktop.

Select an option

Save cami7ord/3d16fb7c7860c7991eddab201d1ca238 to your computer and use it in GitHub Desktop.
Stratipy Test Client — x402 payment flow over Solana
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Stratipy Test Client</title>
<script src="https://unpkg.com/@solana/web3.js@1/lib/index.iife.min.js"></script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f5f5;
color: #333;
display: flex;
justify-content: center;
padding: 2rem;
}
.container { max-width: 640px; width: 100%; }
h1 { font-size: 1.4rem; margin-bottom: 1rem; }
h2 { font-size: 1.1rem; margin-bottom: 0.5rem; color: #555; }
/* Wallet bar */
#wallet-bar {
display: none;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
padding: 0.5rem 0.75rem;
background: #fff;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 0.85rem;
}
#wallet-bar .wallet-label { color: #888; }
#wallet-bar .wallet-address { font-family: monospace; color: #333; }
#connect-wallet-btn {
padding: 0.4rem 0.8rem;
background: #7c3aed;
color: #fff;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
}
#connect-wallet-btn:hover { background: #6d28d9; }
#connect-wallet-btn:disabled { opacity: 0.5; cursor: default; }
#wallet-status { font-size: 0.85rem; }
#wallet-status.error { color: #dc2626; }
#wallet-status.success { color: #15803d; }
#wallet-status.pending { color: #888; }
/* Strategy selector */
#strategy-list { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1.5rem; }
.strategy-btn {
padding: 0.5rem 1rem;
border: 2px solid #ddd;
border-radius: 8px;
background: #fff;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.15s;
display: flex;
align-items: center;
gap: 0.4rem;
}
.strategy-btn:hover { border-color: #888; }
.strategy-btn.selected { border-color: #4a90d9; background: #eef4fc; }
.price-badge {
font-size: 0.7rem;
background: #f0fdf4;
color: #15803d;
padding: 0.15rem 0.4rem;
border-radius: 4px;
font-weight: 600;
border: 1px solid #bbf7d0;
}
/* Payment banner */
#payment-banner {
display: none;
padding: 0.6rem 0.75rem;
margin-bottom: 0.75rem;
border-radius: 8px;
font-size: 0.85rem;
background: #fefce8;
border: 1px solid #fde68a;
color: #854d0e;
}
#payment-banner.success {
background: #f0fdf4;
border-color: #bbf7d0;
color: #15803d;
}
#payment-banner.error {
background: #fef2f2;
border-color: #fecaca;
color: #991b1b;
}
#payment-banner a { color: inherit; font-weight: 600; }
/* Chat area */
#chat-section { display: none; }
#messages {
background: #fff;
border: 1px solid #ddd;
border-radius: 8px;
height: 400px;
overflow-y: auto;
padding: 1rem;
margin-bottom: 0.75rem;
}
.msg {
margin-bottom: 0.75rem;
padding: 0.5rem 0.75rem;
border-radius: 6px;
max-width: 85%;
word-wrap: break-word;
white-space: pre-wrap;
}
.msg.user {
background: #4a90d9;
color: #fff;
margin-left: auto;
}
.msg.ai { background: #e9e9e9; }
.msg.error { background: #fdd; color: #900; }
.msg.system { background: #ffe; color: #666; font-style: italic; text-align: center; max-width: 100%; }
/* Input */
#input-area { display: flex; gap: 0.5rem; }
#message-input {
flex: 1;
padding: 0.6rem 0.75rem;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 0.95rem;
outline: none;
}
#message-input:focus { border-color: #4a90d9; }
#send-btn {
padding: 0.6rem 1.2rem;
background: #4a90d9;
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 0.95rem;
}
#send-btn:disabled { opacity: 0.5; cursor: default; }
#status { margin-top: 0.5rem; font-size: 0.8rem; color: #888; }
/* File upload */
#file-upload-area {
display: none;
padding: 1.5rem;
border: 2px dashed #ccc;
border-radius: 8px;
text-align: center;
background: #fafafa;
margin-bottom: 0.75rem;
}
#file-upload-area label {
cursor: pointer;
color: #4a90d9;
font-weight: 500;
}
#file-upload-area label:hover { text-decoration: underline; }
#file-input { display: none; }
#file-name { margin-top: 0.5rem; font-size: 0.85rem; color: #666; }
/* Select setup */
#select-setup-area {
display: none;
padding: 1.5rem;
border: 1px solid #ddd;
border-radius: 8px;
background: #fafafa;
margin-bottom: 0.75rem;
text-align: center;
}
#select-setup-area label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #555;
}
#select-setup-area select {
padding: 0.5rem 0.75rem;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 0.95rem;
margin-right: 0.5rem;
}
#select-start-btn {
padding: 0.5rem 1rem;
background: #4a90d9;
color: #fff;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.95rem;
}
#select-start-btn:hover { background: #3a7bc8; }
</style>
</head>
<body>
<div class="container">
<h1>Stratipy Test Client</h1>
<div id="wallet-bar">
<span class="wallet-label">Solana Wallet:</span>
<span id="wallet-address" class="wallet-address"></span>
<span id="wallet-status"></span>
<button id="connect-wallet-btn" onclick="connectWallet()">Connect Wallet</button>
</div>
<section id="strategy-section">
<h2>Select a strategy</h2>
<div id="strategy-list"><em>Loading strategies...</em></div>
</section>
<section id="chat-section">
<div id="payment-banner"></div>
<div id="messages"></div>
<div id="file-upload-area">
<label for="file-input"></label>
<input id="file-input" type="file" />
<div id="file-name"></div>
</div>
<div id="select-setup-area">
<label id="select-label"></label>
<select id="select-input"></select>
<button id="select-start-btn">Start</button>
</div>
<div id="input-area">
<input id="message-input" type="text" placeholder="Type a message..." autocomplete="off" />
<button id="send-btn">Send</button>
</div>
<div id="status"></div>
</section>
</div>
<script>
const API_BASE = window.location.origin;
let strategyId = null;
let conversationId = null;
let eventSource = null;
let currentSetup = null;
let strategiesData = [];
let walletPublicKey = null;
const strategyList = document.getElementById('strategy-list');
const chatSection = document.getElementById('chat-section');
const messages = document.getElementById('messages');
const messageInput = document.getElementById('message-input');
const sendBtn = document.getElementById('send-btn');
const status = document.getElementById('status');
const fileUploadArea = document.getElementById('file-upload-area');
const fileInput = document.getElementById('file-input');
const fileName = document.getElementById('file-name');
const inputArea = document.getElementById('input-area');
const selectSetupArea = document.getElementById('select-setup-area');
const selectLabel = document.getElementById('select-label');
const selectInput = document.getElementById('select-input');
const selectStartBtn = document.getElementById('select-start-btn');
const walletBar = document.getElementById('wallet-bar');
const walletAddress = document.getElementById('wallet-address');
const connectWalletBtn = document.getElementById('connect-wallet-btn');
const walletStatus = document.getElementById('wallet-status');
const paymentBanner = document.getElementById('payment-banner');
// --- Solana SPL constants ---
const TOKEN_PROGRAM_ID = new solanaWeb3.PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
const ASSOCIATED_TOKEN_PROGRAM_ID = new solanaWeb3.PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL');
const USDC_MINTS = {
'solana-mainnet': new solanaWeb3.PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'),
'solana-devnet': new solanaWeb3.PublicKey('4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU'),
};
// --- Wallet ---
function getPhantomProvider() {
if (window.phantom?.solana?.isPhantom) return window.phantom.solana;
return null;
}
function setWalletStatus(text, type) {
walletStatus.textContent = text;
walletStatus.className = type || '';
}
async function connectWallet() {
const provider = getPhantomProvider();
if (!provider) {
setWalletStatus('Phantom not detected. Install from phantom.app', 'error');
return;
}
connectWalletBtn.disabled = true;
setWalletStatus('Connecting...', 'pending');
try {
const resp = await provider.connect();
walletPublicKey = resp.publicKey;
const addr = walletPublicKey.toBase58();
walletAddress.textContent = addr.slice(0, 4) + '...' + addr.slice(-4);
connectWalletBtn.style.display = 'none';
setWalletStatus('Connected', 'success');
} catch (e) {
connectWalletBtn.disabled = false;
setWalletStatus('Connection rejected', 'error');
}
}
// --- SPL helpers (no external lib needed) ---
function getAssociatedTokenAddress(walletPk, mintPk) {
const [ata] = solanaWeb3.PublicKey.findProgramAddressSync(
[walletPk.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), mintPk.toBuffer()],
ASSOCIATED_TOKEN_PROGRAM_ID,
);
return ata;
}
function createTransferCheckedInstruction(source, mint, destination, owner, amount, decimals) {
const data = new Uint8Array(10);
const view = new DataView(data.buffer);
data[0] = 12; // TransferChecked instruction index
view.setBigUint64(1, BigInt(amount), true); // little-endian
data[9] = decimals;
const keys = [
{ pubkey: source, isSigner: false, isWritable: true },
{ pubkey: mint, isSigner: false, isWritable: false },
{ pubkey: destination, isSigner: false, isWritable: true },
{ pubkey: owner, isSigner: true, isWritable: false },
];
return new solanaWeb3.TransactionInstruction({
keys,
programId: TOKEN_PROGRAM_ID,
data,
});
}
// --- Payment banner ---
function showPaymentBanner(html, type) {
paymentBanner.innerHTML = html;
paymentBanner.className = type || '';
paymentBanner.style.display = 'block';
}
function hidePaymentBanner() {
paymentBanner.style.display = 'none';
paymentBanner.className = '';
paymentBanner.innerHTML = '';
}
function explorerUrl(txId, network) {
const cluster = network === 'solana-mainnet' ? '' : '?cluster=devnet';
return `https://explorer.solana.com/tx/${txId}${cluster}`;
}
// --- Strategies ---
async function loadStrategies() {
try {
const res = await fetch(`${API_BASE}/strategies`);
const data = await res.json();
strategiesData = data;
strategyList.innerHTML = '';
const hasPaid = data.some(s => s.price != null || s.messagePrice != null);
if (hasPaid) walletBar.style.display = 'flex';
data.forEach(s => {
const btn = document.createElement('button');
btn.className = 'strategy-btn';
const nameSpan = document.createElement('span');
nameSpan.textContent = s.name || s.id;
btn.appendChild(nameSpan);
if (s.price != null) {
const badge = document.createElement('span');
badge.className = 'price-badge';
badge.textContent = (s.price / 1_000_000).toFixed(2) + ' USDC';
btn.appendChild(badge);
}
if (s.messagePrice != null) {
const badge = document.createElement('span');
badge.className = 'price-badge';
badge.textContent = (s.messagePrice / 1_000_000).toFixed(2) + ' USDC/msg';
btn.appendChild(badge);
}
btn.title = s.description || '';
btn.addEventListener('click', () => selectStrategy(s.id, btn));
strategyList.appendChild(btn);
});
} catch (e) {
strategyList.innerHTML = '<em>Failed to load strategies</em>';
}
}
function getStrategy(id) {
return strategiesData.find(s => s.id === id);
}
function selectStrategy(id, btn) {
document.querySelectorAll('.strategy-btn').forEach(b => b.classList.remove('selected'));
btn.classList.add('selected');
strategyId = id;
currentSetup = getStrategy(id)?.setup || null;
startConversation();
}
async function startConversation() {
if (eventSource) { eventSource.close(); eventSource = null; }
messages.innerHTML = '';
fileInput.value = '';
fileName.textContent = '';
hidePaymentBanner();
chatSection.style.display = 'block';
// Hide all input areas first
fileUploadArea.style.display = 'none';
selectSetupArea.style.display = 'none';
inputArea.style.display = 'none';
if (currentSetup?.type === 'file') {
document.querySelector('#file-upload-area label').textContent = currentSetup.label;
fileInput.setAttribute('accept', currentSetup.accept);
fileUploadArea.style.display = 'block';
} else if (currentSetup?.type === 'select') {
selectLabel.textContent = currentSetup.label;
selectInput.innerHTML = '';
currentSetup.options.forEach(opt => {
const option = document.createElement('option');
option.value = opt.value;
option.textContent = opt.label;
selectInput.appendChild(option);
});
selectSetupArea.style.display = 'block';
} else {
inputArea.style.display = 'flex';
}
messageInput.disabled = false;
sendBtn.disabled = false;
setStatus('Starting conversation...');
try {
const res = await fetch(`${API_BASE}/strategies/${strategyId}/conversations`, {
method: 'POST'
});
if (res.status === 402) {
await handlePaymentFlow(res);
return;
}
const data = await res.json();
conversationId = data.conversationId;
if (currentSetup?.type === 'file') {
setStatus('Select a file to begin.');
} else if (currentSetup?.type === 'select') {
setStatus('Choose an option and click Start.');
} else {
setStatus('Connected. Send a message to begin.');
messageInput.focus();
}
} catch (e) {
setStatus('Failed to start conversation');
}
}
// --- x402 Payment Flow ---
/**
* Signs a Solana payment from a 402 response.
* Returns the base64-encoded PAYMENT-SIGNATURE header value, or null on failure.
*/
async function signPayment(response402) {
const provider = getPhantomProvider();
if (!provider) {
showPaymentBanner(
'Phantom wallet not found. Install it from ' +
'<a href="https://phantom.app" target="_blank">phantom.app</a>', 'error',
);
setStatus('');
return null;
}
// Auto-connect if not yet connected
if (!walletPublicKey) {
try {
const resp = await provider.connect();
walletPublicKey = resp.publicKey;
walletAddress.textContent = walletPublicKey.toBase58().slice(0, 4) + '...' +
walletPublicKey.toBase58().slice(-4);
connectWalletBtn.style.display = 'none';
} catch (e) {
showPaymentBanner('Wallet connection rejected', 'error');
setStatus('');
return null;
}
}
// Decode PAYMENT-REQUIRED header
const paymentRequiredB64 = response402.headers.get('PAYMENT-REQUIRED');
if (!paymentRequiredB64) {
showPaymentBanner('Missing payment requirements from server', 'error');
setStatus('');
return null;
}
let paymentRequired;
try {
paymentRequired = JSON.parse(atob(paymentRequiredB64));
} catch (e) {
showPaymentBanner('Failed to decode payment requirements', 'error');
setStatus('');
return null;
}
// Find Solana payment option
const req = paymentRequired.accepts.find(
a => a.scheme === 'exact' && a.network.startsWith('solana'),
);
if (!req) {
showPaymentBanner('No compatible Solana payment option available', 'error');
setStatus('');
return null;
}
const amountUsdc = (parseInt(req.maxAmountRequired) / 1_000_000).toFixed(2);
const networkLabel = req.network === 'solana-mainnet' ? 'Mainnet' : 'Devnet';
showPaymentBanner(
`Payment required: ${amountUsdc} USDC on Solana ${networkLabel}. Confirm in your wallet...`,
);
setStatus('Awaiting wallet signature...');
const mint = USDC_MINTS[req.network];
if (!mint) {
showPaymentBanner(`Unsupported network: ${req.network}`, 'error');
setStatus('');
return null;
}
try {
const rpcUrl = req.network === 'solana-mainnet'
? 'https://api.mainnet-beta.solana.com'
: 'https://api.devnet.solana.com';
const connection = new solanaWeb3.Connection(rpcUrl);
const recipientPk = new solanaWeb3.PublicKey(req.payTo);
const senderAta = getAssociatedTokenAddress(walletPublicKey, mint);
const recipientAta = getAssociatedTokenAddress(recipientPk, mint);
const ix = createTransferCheckedInstruction(
senderAta, mint, recipientAta, walletPublicKey,
parseInt(req.maxAmountRequired), 6,
);
const { blockhash } = await connection.getLatestBlockhash();
const tx = new solanaWeb3.Transaction();
tx.recentBlockhash = blockhash;
tx.feePayer = walletPublicKey;
tx.add(ix);
const signed = await provider.signTransaction(tx);
const serializedTx = btoa(String.fromCharCode(...signed.serialize()));
const paymentPayload = {
x402Version: paymentRequired.x402Version,
scheme: req.scheme,
network: req.network,
payload: { transaction: serializedTx },
};
return btoa(JSON.stringify(paymentPayload));
} catch (e) {
if (e.message?.includes('User rejected')) {
showPaymentBanner('Transaction rejected by user', 'error');
} else {
showPaymentBanner(`Payment error: ${e.message}`, 'error');
}
setStatus('');
return null;
}
}
/** Shows payment-response banner with explorer link if available. */
function showPaymentResponseBanner(response) {
const paymentResponseB64 = response.headers.get('PAYMENT-RESPONSE');
let txLink = '';
if (paymentResponseB64) {
try {
const pr = JSON.parse(atob(paymentResponseB64));
if (pr.transactionId) {
const network = pr.network || 'solana-devnet';
txLink = ` <a href="${explorerUrl(pr.transactionId, network)}" target="_blank">View on Explorer</a>`;
}
} catch (_) {}
}
showPaymentBanner(`Payment successful!${txLink}`, 'success');
}
async function handlePaymentFlow(response402) {
const signatureHeader = await signPayment(response402);
if (!signatureHeader) return;
showPaymentBanner('Submitting payment...');
setStatus('Submitting payment to server...');
try {
const paidRes = await fetch(`${API_BASE}/strategies/${strategyId}/conversations`, {
method: 'POST',
headers: { 'PAYMENT-SIGNATURE': signatureHeader },
});
if (!paidRes.ok) {
const errText = await paidRes.text();
showPaymentBanner(`Payment failed: ${errText}`, 'error');
setStatus('Payment failed');
return;
}
showPaymentResponseBanner(paidRes);
const data = await paidRes.json();
conversationId = data.conversationId;
setStatus('Connected. Send a message to begin.');
messageInput.focus();
} catch (e) {
showPaymentBanner(`Payment error: ${e.message}`, 'error');
setStatus('');
}
}
fileInput.addEventListener('change', async () => {
const file = fileInput.files[0];
if (!file || !conversationId) return;
fileName.textContent = file.name;
setStatus('Uploading CSV...');
const reader = new FileReader();
reader.onload = async (e) => {
const csvText = e.target.result;
appendMessage(`Uploaded: ${file.name}`, 'user');
try {
await fetch(`${API_BASE}/strategies/${strategyId}/conversations/${conversationId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: csvText })
});
fileUploadArea.style.display = 'none';
inputArea.style.display = 'flex';
currentSetup = null;
connectSSE();
} catch (err) {
appendMessage('Failed to upload file', 'error');
setStatus('Upload failed');
}
};
reader.readAsText(file);
});
selectStartBtn.addEventListener('click', async () => {
const selected = selectInput.value;
if (!selected || !conversationId) return;
appendMessage(selected, 'user');
setStatus('Starting...');
try {
await fetch(`${API_BASE}/strategies/${strategyId}/conversations/${conversationId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: selected })
});
selectSetupArea.style.display = 'none';
inputArea.style.display = 'flex';
currentSetup = null;
connectSSE();
} catch (err) {
appendMessage('Failed to start', 'error');
setStatus('Failed');
}
});
async function sendMessage() {
const text = messageInput.value.trim();
if (!text || !conversationId) return;
messageInput.value = '';
appendMessage(text, 'user');
sendBtn.disabled = true;
setStatus('Waiting for AI...');
const url = `${API_BASE}/strategies/${strategyId}/conversations/${conversationId}`;
const body = JSON.stringify({ text });
const headers = { 'Content-Type': 'application/json' };
try {
let res = await fetch(url, { method: 'POST', headers, body });
if (res.status === 402) {
const signatureHeader = await signPayment(res);
if (!signatureHeader) {
sendBtn.disabled = false;
return;
}
showPaymentBanner('Submitting payment...');
setStatus('Submitting payment to server...');
res = await fetch(url, {
method: 'POST',
headers: { ...headers, 'PAYMENT-SIGNATURE': signatureHeader },
body,
});
if (!res.ok) {
const errText = await res.text();
showPaymentBanner(`Payment failed: ${errText}`, 'error');
setStatus('Payment failed');
sendBtn.disabled = false;
return;
}
showPaymentResponseBanner(res);
}
connectSSE();
} catch (e) {
appendMessage('Failed to send message', 'error');
sendBtn.disabled = false;
}
}
function connectSSE() {
if (eventSource) eventSource.close();
const url = `${API_BASE}/strategies/${strategyId}/conversations/${conversationId}/events`;
eventSource = new EventSource(url);
eventSource.onmessage = (e) => {
try {
const data = JSON.parse(e.data);
appendMessage(data.text, 'ai');
sendBtn.disabled = false;
setStatus('');
} catch (err) {
appendMessage(e.data, 'ai');
}
};
eventSource.addEventListener('finish', () => {
appendMessage('Conversation ended.', 'system');
messageInput.disabled = true;
sendBtn.disabled = true;
setStatus('Finished');
eventSource.close();
eventSource = null;
});
eventSource.onerror = () => {
// EventSource auto-reconnects; close it if conversation is done
if (messageInput.disabled) {
eventSource.close();
eventSource = null;
}
};
}
function appendMessage(text, type) {
const div = document.createElement('div');
div.className = `msg ${type}`;
div.textContent = text;
messages.appendChild(div);
messages.scrollTop = messages.scrollHeight;
}
function setStatus(text) {
status.textContent = text;
}
sendBtn.addEventListener('click', sendMessage);
messageInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !sendBtn.disabled) sendMessage();
});
// Pick up existing Phantom connection on page load
async function checkExistingConnection() {
const provider = getPhantomProvider();
if (!provider) return;
try {
const resp = await provider.connect({ onlyIfTrusted: true });
walletPublicKey = resp.publicKey;
const addr = walletPublicKey.toBase58();
walletAddress.textContent = addr.slice(0, 4) + '...' + addr.slice(-4);
connectWalletBtn.style.display = 'none';
setWalletStatus('Connected', 'success');
} catch (_) {
// Not previously connected — user will click the button
}
}
loadStrategies();
checkExistingConnection();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment