// ==UserScript==
// @name RugPlay Universal API GUI
// @namespace http://tampermonkey.net/
// @version 1.01
// @description A GUI for the Rugpull API.
// @author 4koy
// @match https://rugplay.com/*
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// TODO: maybe make this configurable later idk
let apiKey = GM_getValue('rugplay_api_key', '');
// main gui stuff
function createGUI() {
const gui = document.createElement('div');
gui.id = 'rugplay-api-gui';
gui.innerHTML = `
<div class="api-header">
<h2>RugPlay *UNIVERSE Utility</h2>
<button id="toggle-gui">−</button>
</div>
<div class="api-content">
<div class="api-key-section">
<label for="api-key-input">API Key:</label>
<input type="password" id="api-key-input" value="${apiKey}" placeholder="Enter your API key">
<button id="save-key">Save</button>
</div>
<div class="endpoint-section">
<label for="endpoint-select">Endpoint:</label>
<select id="endpoint-select">
<option value="stability">Coin Stability Analysis</option>
<option value="top">Top Coins</option>
<option value="market">Market Data</option>
<option value="coin">Coin Details</option>
<option value="holders">Coin Holders</option>
<option value="hopium">Prediction Markets</option>
<option value="hopium-detail">Prediction Market Detail</option>
</select>
</div>
<div id="parameters-section"></div>
<div class="action-section">
<button id="make-request">Make Request</button>
<button id="clear-response">Clear Response</button>
</div>
<div class="response-section">
<div class="response-header">
<span>Response:</span>
<div class="response-controls">
<span id="request-status"></span>
<button id="copy-output-small" title="Copy to clipboard">📋</button>
</div>
</div>
<pre id="response-output"></pre>
</div>
</div>
`;
document.body.appendChild(gui);
return gui;
}
// styling - going for that sleek glassmorphism vibe
GM_addStyle(`
#rugplay-api-gui {
position: fixed;
top: 20px;
right: 20px;
width: 405px;
max-height: 108vh;
background: rgba(15, 15, 23, 0.85);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 14px;
box-shadow:
0 7px 29px rgba(0, 0, 0, 0.4),
0 0 0 1px rgba(255, 255, 255, 0.05),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
z-index: 10000;
font-family: 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif;
color: rgba(255, 255, 255, 0.9);
overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transform: scale(0.9);
transform-origin: top right;
}
#rugplay-api-gui:hover {
box-shadow:
0 11px 36px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(255, 255, 255, 0.08),
inset 0 1px 0 rgba(255, 255, 255, 0.15);
}
.api-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 22px;
background: rgba(255, 255, 255, 0.05);
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
backdrop-filter: blur(10px);
}
.api-header h2 {
margin: 0;
font-size: 14px;
font-weight: 600;
background: linear-gradient(135deg, #ffffff 0%, #a0a0a0 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
#toggle-gui {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
cursor: pointer;
padding: 7px 11px;
border-radius: 7px;
transition: all 0.2s ease;
backdrop-filter: blur(10px);
}
#toggle-gui:hover {
background: rgba(255, 255, 255, 0.12);
border-color: rgba(255, 255, 255, 0.2);
color: rgba(255, 255, 255, 1);
transform: scale(1.05);
}
.api-content {
padding: 22px;
max-height: 99vh;
overflow-y: auto;
}
.api-content::-webkit-scrollbar {
width: 6px;
}
.api-content::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 3px;
}
.api-content::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
}
.api-content::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
.api-content.collapsed {
display: none;
}
.api-key-section, .endpoint-section, .action-section {
margin-bottom: 22px;
}
.api-key-section {
display: flex;
gap: 11px;
align-items: center;
}
label {
font-weight: 500;
margin-bottom: 7px;
display: block;
color: rgba(255, 255, 255, 0.8);
font-size: 13px;
}
.api-key-section label {
margin-bottom: 0;
white-space: nowrap;
}
input, select, textarea {
width: 100%;
padding: 11px 14px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 9px;
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(10px);
color: rgba(255, 255, 255, 0.9);
font-size: 13px;
transition: all 0.2s ease;
}
input::placeholder {
color: rgba(255, 255, 255, 0.4);
}
input:focus, select:focus, textarea:focus {
outline: none;
background: rgba(255, 255, 255, 0.12);
border-color: rgba(99, 102, 241, 0.5);
box-shadow:
0 0 0 3px rgba(99, 102, 241, 0.1),
0 4px 14px rgba(99, 102, 241, 0.1);
}
button {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
color: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(255, 255, 255, 0.15);
padding: 11px 18px;
border-radius: 9px;
cursor: pointer;
font-weight: 500;
font-size: 13px;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
button:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.25);
transform: translateY(-1px);
box-shadow: 0 5px 18px rgba(0, 0, 0, 0.3);
}
button:active {
transform: translateY(0);
}
#save-key {
flex-shrink: 0;
}
.parameter-group {
margin-bottom: 18px;
}
.parameter-row {
display: flex;
gap: 11px;
margin-bottom: 11px;
}
.parameter-row input, .parameter-row select {
flex: 1;
}
.action-section {
display: flex;
gap: 11px;
}
#make-request {
background: linear-gradient(135deg, rgba(34, 197, 94, 0.9) 0%, rgba(22, 163, 74, 0.9) 100%);
border: 1px solid rgba(34, 197, 94, 0.3);
flex: 1;
}
#make-request:hover {
background: linear-gradient(135deg, rgba(34, 197, 94, 1) 0%, rgba(22, 163, 74, 1) 100%);
border-color: rgba(34, 197, 94, 0.5);
box-shadow: 0 6px 25px rgba(34, 197, 94, 0.3);
}
#clear-response {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.9) 0%, rgba(220, 38, 38, 0.9) 100%);
border: 1px solid rgba(239, 68, 68, 0.3);
}
#clear-response:hover {
background: linear-gradient(135deg, rgba(239, 68, 68, 1) 0%, rgba(220, 38, 38, 1) 100%);
border-color: rgba(239, 68, 68, 0.5);
box-shadow: 0 6px 25px rgba(239, 68, 68, 0.3);
}
.response-section {
margin-top: 22px;
}
.response-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 11px;
}
.response-controls {
display: flex;
gap: 11px;
align-items: center;
}
#request-status {
font-size: 11px;
padding: 5px 11px;
border-radius: 7px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.8);
}
#response-output {
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(15px);
color: rgba(255, 255, 255, 0.9);
padding: 18px;
border-radius: 11px;
border: 1px solid rgba(255, 255, 255, 0.08);
white-space: pre-wrap;
word-wrap: break-word;
max-height: 450px;
overflow-y: auto;
font-size: 10px;
line-height: 1.0;
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
box-shadow: inset 0 2px 7px rgba(0, 0, 0, 0.3);
}
#response-output::-webkit-scrollbar {
width: 6px;
}
#response-output::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 3px;
}
#response-output::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
}
#copy-output-small {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 7px 11px;
font-size: 13px;
min-width: auto;
border-radius: 7px;
}
#copy-output-small:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.2);
}
.parameter-description {
font-size: 10px;
color: rgba(255, 255, 255, 0.5);
margin-top: 4px;
font-style: italic;
}
/* Subtle glow animations */
@keyframes subtle-glow {
0%, 100% { box-shadow: 0 0 20px rgba(99, 102, 241, 0.1); }
50% { box-shadow: 0 0 30px rgba(99, 102, 241, 0.2); }
}
#rugplay-api-gui:focus-within {
animation: subtle-glow 3s ease-in-out infinite;
}
`);
// endpoint configs - these match the API docs
const endpointConfigs = {
top: {
url: 'https://rugplay.com/api/v1/top',
method: 'GET',
parameters: []
},
market: {
url: 'https://rugplay.com/api/v1/market',
method: 'GET',
parameters: [
{ name: 'search', type: 'text', description: 'Search by coin name or symbol' },
{ name: 'sortBy', type: 'select', options: ['marketCap', 'currentPrice', 'change24h', 'volume24h', 'createdAt'], description: 'Sort field (default: marketCap)' },
{ name: 'sortOrder', type: 'select', options: ['asc', 'desc'], description: 'Sort order (default: desc)' },
{ name: 'priceFilter', type: 'select', options: ['all', 'under1', '1to10', '10to100', 'over100'], description: 'Price range filter' },
{ name: 'changeFilter', type: 'select', options: ['all', 'gainers', 'losers', 'hot', 'wild'], description: 'Change filter' },
{ name: 'page', type: 'number', description: 'Page number (default: 1)' },
{ name: 'limit', type: 'number', description: 'Items per page, max 100 (default: 12)' }
]
},
coin: {
url: 'https://rugplay.com/api/v1/coin/{symbol}',
method: 'GET',
parameters: [
{ name: 'symbol', type: 'text', required: true, description: 'Coin symbol (e.g., "TEST")' },
{ name: 'timeframe', type: 'select', options: ['1m', '5m', '15m', '1h', '4h', '1d'], description: 'Chart timeframe (default: 1m)' }
]
},
holders: {
url: 'https://rugplay.com/api/v1/holders/{symbol}',
method: 'GET',
parameters: [
{ name: 'symbol', type: 'text', required: true, description: 'Coin symbol (e.g., "TEST")' },
{ name: 'limit', type: 'number', description: 'Number of holders to return, max 200 (default: 50)' }
]
},
stability: {
url: 'custom',
method: 'GET',
parameters: [
{ name: 'symbol', type: 'text', required: true, description: 'Coin symbol (e.g., "UNIVERSE")' }
]
},
hopium: {
url: 'https://rugplay.com/api/v1/hopium',
method: 'GET',
parameters: [
{ name: 'status', type: 'select', options: ['ACTIVE', 'RESOLVED', 'CANCELLED', 'ALL'], description: 'Filter by status (default: ACTIVE)' },
{ name: 'page', type: 'number', description: 'Page number (default: 1)' },
{ name: 'limit', type: 'number', description: 'Items per page, max 100 (default: 20)' }
]
},
'hopium-detail': {
url: 'https://rugplay.com/api/v1/hopium/{question_id}',
method: 'GET',
parameters: [
{ name: 'question_id', type: 'number', required: true, description: 'Question ID (e.g., 101)' }
]
}
};
// main initialization function
function initializeGUI() {
const gui = createGUI();
// grab all the elements we need
const toggleBtn = document.getElementById('toggle-gui');
const apiContent = document.querySelector('.api-content');
const apiKeyInput = document.getElementById('api-key-input');
const saveKeyBtn = document.getElementById('save-key');
const endpointSelect = document.getElementById('endpoint-select');
const parametersSection = document.getElementById('parameters-section');
const makeRequestBtn = document.getElementById('make-request');
const clearResponseBtn = document.getElementById('clear-response');
const responseOutput = document.getElementById('response-output');
const requestStatus = document.getElementById('request-status');
// toggle gui collapse/expand
toggleBtn.addEventListener('click', () => {
if (apiContent.classList.contains('collapsed')) {
apiContent.classList.remove('collapsed');
toggleBtn.textContent = '−';
} else {
apiContent.classList.add('collapsed');
toggleBtn.textContent = '+';
}
});
// save the api key
saveKeyBtn.addEventListener('click', () => {
apiKey = apiKeyInput.value;
GM_setValue('rugplay_api_key', apiKey);
requestStatus.textContent = 'API key saved';
requestStatus.style.background = '#4CAF50';
});
// when endpoint changes, update the parameter fields
endpointSelect.addEventListener('change', updateParameters);
// make the actual request
makeRequestBtn.addEventListener('click', makeAPIRequest);
// clear response output
clearResponseBtn.addEventListener('click', () => {
responseOutput.textContent = '';
requestStatus.textContent = '';
});
// copy functionality - this was a pain to get working properly
const copyOutputBtn = document.getElementById('copy-output-small');
copyOutputBtn.addEventListener('click', () => {
const outputText = responseOutput.textContent;
if (!outputText) {
requestStatus.textContent = 'No output to copy';
requestStatus.style.background = '#FF9800';
return;
}
// try modern clipboard API first
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(outputText).then(() => {
requestStatus.textContent = 'Output copied to clipboard!';
requestStatus.style.background = '#4CAF50';
setTimeout(() => {
requestStatus.textContent = '';
}, 2000);
}).catch(() => {
requestStatus.textContent = 'Failed to copy output';
requestStatus.style.background = '#f44336';
});
} else {
// fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = outputText;
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
requestStatus.textContent = 'Output copied to clipboard!';
requestStatus.style.background = '#4CAF50';
setTimeout(() => {
requestStatus.textContent = '';
}, 2000);
} catch (err) {
requestStatus.textContent = 'Failed to copy output';
requestStatus.style.background = '#f44336';
}
document.body.removeChild(textArea);
}
});
// set up initial parameters
updateParameters();
function updateParameters() {
const endpoint = endpointSelect.value;
const config = endpointConfigs[endpoint];
parametersSection.innerHTML = '';
if (config.parameters.length > 0) {
config.parameters.forEach(param => {
const paramDiv = document.createElement('div');
paramDiv.className = 'parameter-group';
let inputHTML = '';
if (param.type === 'select') {
inputHTML = `<select id="param-${param.name}">
<option value="">-- Select ${param.name} --</option>
${param.options.map(opt => `<option value="${opt}">${opt}</option>`).join('')}
</select>`;
} else {
inputHTML = `<input type="${param.type}" id="param-${param.name}" placeholder="${param.description}">`;
}
paramDiv.innerHTML = `
<label for="param-${param.name}">${param.name}${param.required ? ' *' : ''}:</label>
${inputHTML}
<div class="parameter-description">${param.description}</div>
`;
parametersSection.appendChild(paramDiv);
});
}
}
function makeAPIRequest() {
const endpoint = endpointSelect.value;
// handle custom stability analysis
if (endpoint === 'stability') {
makeStabilityAnalysis();
return;
}
const config = endpointConfigs[endpoint];
if (!apiKey) {
requestStatus.textContent = 'Please enter an API key';
requestStatus.style.background = '#f44336';
return;
}
// build the url with parameters
let url = config.url;
const queryParams = [];
config.parameters.forEach(param => {
const value = document.getElementById(`param-${param.name}`).value;
if (param.required && !value) {
requestStatus.textContent = `Required parameter missing: ${param.name}`;
requestStatus.style.background = '#f44336';
return;
}
if (value) {
if (url.includes(`{${param.name}}`)) {
url = url.replace(`{${param.name}}`, value);
} else {
queryParams.push(`${param.name}=${encodeURIComponent(value)}`);
}
}
});
if (queryParams.length > 0) {
url += '?' + queryParams.join('&');
}
requestStatus.textContent = 'Making request...';
requestStatus.style.background = '#2196F3';
GM_xmlhttpRequest({
method: config.method,
url: url,
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
onload: function(response) {
try {
const jsonResponse = JSON.parse(response.responseText);
responseOutput.textContent = JSON.stringify(jsonResponse, null, 2);
requestStatus.textContent = `Success (${response.status})`;
requestStatus.style.background = '#4CAF50';
} catch (e) {
responseOutput.textContent = response.responseText;
requestStatus.textContent = `Response (${response.status})`;
requestStatus.style.background = '#FF9800';
}
},
onerror: function(error) {
responseOutput.textContent = JSON.stringify(error, null, 2);
requestStatus.textContent = 'Error';
requestStatus.style.background = '#f44336';
}
});
}
function makeStabilityAnalysis() {
const symbol = document.getElementById('param-symbol').value;
if (!symbol) {
requestStatus.textContent = 'Symbol is required for stability analysis';
requestStatus.style.background = '#f44336';
return;
}
requestStatus.textContent = 'Analyzing coin stability...';
requestStatus.style.background = '#2196F3';
// get both coin and holder data
Promise.all([
fetchAPI(`https://rugplay.com/api/v1/coin/${symbol}`),
fetchAPI(`https://rugplay.com/api/v1/holders/${symbol}`)
]).then(([coinData, holdersData]) => {
const analysis = generateStabilityAnalysis(coinData, holdersData, symbol);
displayStabilityAnalysis(analysis);
requestStatus.textContent = 'Stability analysis complete';
requestStatus.style.background = '#4CAF50';
}).catch(error => {
responseOutput.textContent = `Error fetching data: ${error.message}`;
requestStatus.textContent = 'Analysis failed';
requestStatus.style.background = '#f44336';
});
}
function fetchAPI(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: url,
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
onload: function(response) {
try {
const data = JSON.parse(response.responseText);
resolve(data);
} catch (e) {
reject(new Error(`Failed to parse response: ${e.message}`));
}
},
onerror: function(error) {
reject(new Error('Network error'));
}
});
});
}
function generateStabilityAnalysis(coinData, holdersData, symbol) {
const coin = coinData.coin;
const holders = holdersData.holders;
// crunch the numbers
const topHolderPercentage = holders[0]?.percentage || 0;
const top5Percentage = holders.slice(0, 5).reduce((sum, holder) => sum + (holder.percentage || 0), 0);
const poolPercentage = ((coin.poolCoinAmount / coin.circulatingSupply) * 100) || 0;
// basic price trend stuff
const priceChange = coin.change24h || 0;
const priceChangePercent = ((priceChange / coin.currentPrice) * 100) || 0;
// make a prediction
let prediction = 'STABLE';
let predictionColor = '#4CAF50';
let predictionText = 'Stable - Normal trading patterns';
if (priceChangePercent > 100) {
prediction = 'UP';
predictionColor = '#2196F3';
predictionText = `UP - Uptrend, last 10 candles up ${Math.abs(priceChangePercent).toFixed(2)}%`;
} else if (priceChangePercent < -50) {
prediction = 'DOWN';
predictionColor = '#f44336';
predictionText = `DOWN - Downtrend, last 10 candles down ${Math.abs(priceChangePercent).toFixed(2)}%`;
}
// calculate rug risk - this is where it gets spicy
let riskLevel = 'LOW';
let riskColor = '#4CAF50';
let riskFactors = [];
if (topHolderPercentage > 70) {
riskLevel = 'HIGH';
riskColor = '#f44336';
riskFactors.push(`Top holder owns ${topHolderPercentage.toFixed(2)}% (+8%)`);
} else if (topHolderPercentage > 50) {
riskLevel = 'MEDIUM';
riskColor = '#FF9800';
riskFactors.push(`Top holder owns ${topHolderPercentage.toFixed(2)}% (+6%)`);
} else {
riskFactors.push(`Top holder owns ${topHolderPercentage.toFixed(2)}% (+0%)`);
}
if (top5Percentage > 80) {
riskFactors.push(`Top 5 holders own ${top5Percentage.toFixed(2)}% (+8%)`);
if (riskLevel === 'LOW') riskLevel = 'MEDIUM';
} else {
riskFactors.push(`Top 5 holders own ${top5Percentage.toFixed(2)}% (+0%)`);
}
if (poolPercentage > 15) {
riskFactors.push(`Pool is healthy (${poolPercentage.toFixed(3)}% of supply) (+0%)`);
} else {
riskFactors.push(`Pool is low (${poolPercentage.toFixed(3)}% of supply) (+4%)`);
if (riskLevel === 'LOW') riskLevel = 'MEDIUM';
}
if (Math.abs(priceChangePercent) < 20) {
riskFactors.push('No major recent price drop (+0%)');
} else {
riskFactors.push(`Major price movement detected (+2%)`);
}
return {
symbol,
coin,
prediction,
predictionColor,
predictionText,
riskLevel,
riskColor,
riskFactors,
topHolderPercentage,
top5Percentage,
poolPercentage
};
}
function displayStabilityAnalysis(analysis) {
// fancy ascii output because why not
const output = `
╔═══════════════════════════════════════════════╗
${analysis.symbol.toUpperCase()} STABILITY ANALYSIS
╚═══════════════════════════════════════════════╝
📊 BASIC INFO:
Current Price: ${analysis.coin.currentPrice?.toFixed(6) || 'N/A'}
24h Change: ${analysis.coin.change24h?.toLocaleString() || 'N/A'}
Market Cap: ${analysis.coin.marketCap?.toLocaleString() || 'N/A'}
Volume 24h: ${analysis.coin.volume24h?.toLocaleString() || 'N/A'}
🔮 PREDICTION: ${analysis.predictionText}
⚠️ RUGPULL RISK: ${analysis.riskLevel} - ${analysis.riskFactors.join('; ')}
📋 RISK BREAKDOWN:
${analysis.riskFactors.join('\n ')}
🏆 HOLDER ANALYSIS:
Top Holder: ${analysis.topHolderPercentage.toFixed(2)}%
Top 5 Holders: ${analysis.top5Percentage.toFixed(2)}%
Pool Health: ${analysis.poolPercentage.toFixed(3)}% of supply
💡 RECOMMENDATION:
${analysis.riskLevel === 'HIGH' ? '⛔ HIGH RISK - Exercise extreme caution' :
analysis.riskLevel === 'MEDIUM' ? '⚠️ MEDIUM RISK - Monitor closely' :
'✅ LOW RISK - Relatively stable'}
`;
responseOutput.textContent = output;
}
}
// start everything up when ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeGUI);
} else {
initializeGUI();
}
})();