The small automated curation program is getting even better.
Image below
Test Markdown
I don't know how to display code in Markdown, I hope this is correct ^^
<?php
// ----------- CONFIGURATION -----------
define('VOTER_ACCOUNT', 'your-name-account');
define('MIN_VP_THRESHOLD', 78);
define('MIN_WORD_COUNT', 350);
define('POSTS_PER_PAGE', 20);
define('DEBUG_MODE', true);
// ----------- AUTO-VOTE CONFIGURATION -----------
define('AUTO_VOTE_ENABLED', true); // ⚠️ Set false to disable
define('AUTO_VOTE_DELAY', 4000); // Delay between each vote (ms)
define('AUTO_REFRESH_AFTER_VOTE', true); // Refresh after voting all
// ========== 🎯 CHANGE YOUR VOTING THRESHOLDS HERE ==========
// Format: word => %%%% vote (in hundredths, 700 = 7%)
$VOTE_RULES = [
1500 => 700, // 7% for 1500+ words
1000 => 600, // 6% for 1000-1499 words
500 => 500, // 5% for 500-999 words
350 => 400 // 4% for 350-499 words
];
// ========================================================
// ----------- WEIGHT CALCULATION FUNCTION -----------
function get_vote_weight($word_count) {
global $VOTE_RULES;
foreach ($VOTE_RULES as $min_words => $weight) {
if ($word_count >= $min_words) return $weight;
}
return 0;
}
// ----------- WORDS -----------
function count_words($text) {
$text = preg_replace('/^#+\s*.+/m', '', $text);
$text = preg_replace('/\[[^\]]*\]\([^)]*\)/', '', $text);
$text = preg_replace('/!\[[^\]]*\]\([^)]*\)/', '', $text);
$text = preg_replace('/`{1,3}[^`]+`{1,3}/', '', $text);
$text = strip_tags($text);
$text = html_entity_decode($text, ENT_QUOTES, 'UTF-8');
$text = preg_replace('/[^\p{L}\p{N}\s\']/u', ' ', $text);
$text = preg_replace('/\s+/', ' ', trim($text));
$words = explode(' ', $text);
return count($words);
}
// ----------- POSTS -----------
function get_hive_posts() {
$url = "https://api.hive.blog";
$payload = json_encode([
"jsonrpc" => "2.0",
"method" => "bridge.get_ranked_posts",
"params" => [
"sort" => "created",
"tag" => "",
"observer" => VOTER_ACCOUNT,
"limit" => POSTS_PER_PAGE
],
"id" => 1
]);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
CURLOPT_SSL_VERIFYPEER => false,
]);
$res = curl_exec($ch);
curl_close($ch);
$json = json_decode($res, true);
return $json['result'] ?? [];
}
// ----------- ACCOUNT -----------
function get_hive_account($account) {
$url = "https://api.hive.blog";
$payload = json_encode([
"jsonrpc" => "2.0",
"method" => "condenser_api.get_accounts",
"params" => [[$account]],
"id" => 1
]);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
CURLOPT_SSL_VERIFYPEER => false,
]);
$res = curl_exec($ch);
curl_close($ch);
$json = json_decode($res, true);
return $json['result'][0] ?? null;
}
// ----------- displayed VP -----------
function get_current_vp($account) {
$url = "https://api.hive.blog";
$payload = json_encode([
"jsonrpc" => "2.0",
"method" => "condenser_api.get_accounts",
"params" => [[$account]],
"id" => 1
]);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
CURLOPT_SSL_VERIFYPEER => false,
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
return isset($response['result'][0]['voting_power'])
? round($response['result'][0]['voting_power'] / 100, 1)
: 0;
}
// ----------- PRINCIPALE -----------
$account = get_hive_account(VOTER_ACCOUNT);
$current_vp = get_current_vp(VOTER_ACCOUNT);
$posts = get_hive_posts();
$eligible_posts = [];
$debug_logs = [];
foreach ($posts as $p) {
$words = count_words($p['body']);
$vote_weight = get_vote_weight($words);
$user_voted = false;
$reason = "";
foreach ($p['active_votes'] ?? [] as $vote) {
if ($vote['voter'] === VOTER_ACCOUNT) {
$user_voted = true;
$reason = "Already voted";
break;
}
}
if ($words < MIN_WORD_COUNT) {
$reason = "Too short ($words words)";
} elseif ($user_voted) {
$reason = "Already voted";
} else {
$reason = "✅ Eligible";
}
$debug_logs[] = [
'author' => $p['author'],
'permlink' => $p['permlink'],
'words' => $words,
'reason' => $reason
];
if ($vote_weight > 0 && !$user_voted && $current_vp >= MIN_VP_THRESHOLD) {
$p['is_eligible'] = true;
$eligible_posts[] = $p;
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🤖 Hive Auto Words</title>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600&family=Rajdhani:wght@500;600&display=swap" rel="stylesheet">
<style>
:root {
--neon-blue: #00d4ff;
--neon-purple: #b200ff;
--neon-green: #00ff88;
--neon-red: #ff2e63;
--neon-yellow: #ffc700;
--bg-dark: #0a0a1a;
--bg-card: #121230;
--text-primary: #e0e0ff;
--text-secondary: #a0a0c0;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Rajdhani', sans-serif;
background: linear-gradient(135deg, #0a0a1a 0%, #1a0a2e 100%);
color: var(--text-primary);
padding: 40px 20px;
min-height: 100vh;
}
h1 {
font-family: 'Orbitron', sans-serif;
text-align: center;
font-size: 2.5rem;
margin-bottom: 40px;
background: linear-gradient(90deg, var(--neon-blue), var(--neon-purple));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 0 20px rgba(0, 212, 255, 0.3);
}
.card {
background: rgba(18, 18, 48, 0.9);
border: 1px solid rgba(178, 0, 255, 0.2);
border-radius: 15px;
padding: 25px;
margin-bottom: 30px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.card h2 {
font-family: 'Orbitron', sans-serif;
color: var(--neon-purple);
margin-bottom: 20px;
font-size: 1.5rem;
}
.card p {
font-size: 1.1rem;
margin-bottom: 12px;
color: var(--text-secondary);
}
.card strong {
color: var(--text-primary);
}
.vp-container {
background: rgba(10, 10, 30, 0.5);
padding: 15px;
border-radius: 10px;
margin: 15px 0;
}
.vp-bar {
width: 100%;
height: 20px;
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
overflow: hidden;
margin-top: 10px;
}
.vp-fill {
height: 100%;
width: <?php echo $current_vp; ?>%;
background: linear-gradient(90deg, var(--neon-green), var(--neon-yellow));
transition: width 0.5s ease;
}
table {
width: 100%;
border-collapse: collapse;
background: rgba(18, 18, 48, 0.6);
border-radius: 10px;
overflow: hidden;
}
th, td {
padding: 15px;
text-align: left;
border-bottom: 1px solid rgba(178, 0, 255, 0.1);
}
th {
background: rgba(178, 0, 255, 0.1);
color: var(--neon-purple);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
font-size: 0.9rem;
}
tr:hover {
background: rgba(0, 212, 255, 0.05);
}
button {
padding: 8px 16px;
border: none;
border-radius: 6px;
font-family: 'Rajdhani', sans-serif;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
background: linear-gradient(90deg, var(--neon-blue), var(--neon-purple));
color: white;
box-shadow: 0 4px 6px rgba(0, 212, 255, 0.2);
position: relative;
overflow: hidden;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 212, 255, 0.3);
}
button:active {
transform: translateY(0);
}
button:disabled {
background: #3a3a5a;
color: #666;
cursor: not-allowed;
box-shadow: none;
}
button.voted {
background: linear-gradient(90deg, var(--neon-green), var(--neon-yellow));
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(0, 255, 136, 0.4); }
70% { box-shadow: 0 0 0 10px rgba(0, 255, 136, 0); }
100% { box-shadow: 0 0 0 0 rgba(0, 255, 136, 0); }
}
.debug-table {
margin-top: 40px;
border-top: 1px solid rgba(178, 0, 255, 0.2);
padding-top: 30px;
}
.debug-table h2 {
color: var(--neon-purple);
text-shadow: 0 0 8px rgba(178, 0, 255, 0.3);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.card, table, h1, h2 {
animation: fadeIn 0.5s ease-out forwards;
}
.history-item {
background: rgba(10, 10, 30, 0.7);
border: 1px solid rgba(178, 0, 255, 0.3);
border-radius: 10px;
padding: 15px;
display: grid;
grid-template-columns: 80px 1fr auto;
gap: 15px;
align-items: center;
transition: all 0.3s ease;
}
.history-item:hover {
background: rgba(0, 212, 255, 0.1);
border-color: var(--neon-blue);
transform: translateX(5px);
box-shadow: 0 4px 15px rgba(0, 212, 255, 0.2);
}
.history-avatar {
width: 60px;
height: 60px;
border-radius: 50%;
border: 2px solid var(--neon-purple);
object-fit: cover;
}
.history-content {
display: flex;
flex-direction: column;
gap: 5px;
}
.history-author {
font-weight: bold;
color: var(--neon-blue);
text-decoration: none;
font-size: 1.1rem;
}
.history-author:hover {
color: var(--neon-purple);
}
.history-post-link {
color: var(--text-secondary);
text-decoration: none;
font-size: 0.9rem;
}
.history-post-link:hover {
color: var(--neon-green);
}
.history-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8px;
}
.history-weight {
background: linear-gradient(90deg, var(--neon-purple), var(--neon-blue));
padding: 6px 12px;
border-radius: 6px;
font-weight: bold;
font-size: 0.95rem;
}
.history-time {
color: var(--text-secondary);
font-size: 0.85rem;
}
.status-badge {
padding: 6px 12px;
border-radius: 6px;
font-size: 0.85rem;
font-weight: 600;
text-align: center;
}
.status-success {
background: rgba(0, 255, 136, 0.2);
color: var(--neon-green);
border: 1px solid rgba(0, 255, 136, 0.3);
}
.status-failed {
background: rgba(255, 46, 99, 0.2);
color: var(--neon-red);
border: 1px solid rgba(255, 46, 99, 0.3);
}
.status-loading {
background: rgba(0, 212, 255, 0.2);
color: var(--neon-blue);
border: 1px solid rgba(0, 212, 255, 0.3);
}
@keyframes pulse-glow {
0%, 100% {
box-shadow: 0 4px 12px rgba(0, 212, 255, 0.3);
border-color: rgba(0, 212, 255, 0.4);
}
50% {
box-shadow: 0 6px 20px rgba(0, 212, 255, 0.6);
border-color: rgba(0, 212, 255, 0.8);
}
}
@media (max-width: 768px) {
body { padding: 15px; }
.card { padding: 15px; }
th, td { padding: 8px 10px; }
h1 { font-size: 1.8rem; }
}
</style>
</head>
<body>
<h1>🤖 HIVE AUTO WORDS <span style="font-size: 1.2rem; font-weight: normal; color: var(--text-secondary);">Edition</span></h1>
<!-- ========== PATCH 2: Account Switcher ========== -->
<div class="card" style="position: sticky; top: 20px; z-index: 1000; margin-bottom: 30px;">
<h2 style="margin-bottom: 15px;">🔐 Voting Account</h2>
<div style="display: flex; gap: 10px; align-items: center; margin-bottom: 10px;">
<input
type="text"
id="account-input"
placeholder="Enter Hive username"
style="flex: 1; padding: 10px; background: rgba(18, 18, 48, 0.8); border: 1px solid rgba(178, 0, 255, 0.3); border-radius: 8px; color: var(--text-primary); font-size: 1rem;"
/>
<button
id="connect-btn"
onclick="connectAccount()"
style="padding: 10px 20px; white-space: nowrap;">
🔗 Connect & Validate
</button>
</div>
<div id="account-status" style="display: none; padding: 10px; border-radius: 6px; margin-top: 10px; font-size: 0.9rem;"></div>
</div>
<div class="card">
<h2>📊 Current Status</h2>
<p><strong>Account:</strong> <span style="color: var(--neon-purple);" id="current-account-display">@<?php echo VOTER_ACCOUNT; ?></span></p>
<div class="vp-container">
<div>
<strong>VP:</strong> <span style="color: <?php echo $current_vp >= 80 ? 'var(--neon-green)' : ($current_vp >= 50 ? 'var(--neon-yellow)' : 'var(--neon-red)'); ?>;">
<?php echo $current_vp; ?>%
</span>
</div>
<div class="vp-bar">
<div class="vp-fill"></div>
</div>
</div>
<p><strong>Eligible Posts:</strong> <span style="color: var(--neon-green);"><?php echo count($eligible_posts); ?></span>/<?php echo count($posts); ?></p>
<!-- ========== AFFICHAGE DES RÈGLES DE VOTE ========== -->
<div style="margin-top: 20px; padding: 15px; background: rgba(0, 212, 255, 0.05); border-radius: 8px; border: 1px solid rgba(0, 212, 255, 0.2);">
<h3 style="color: var(--neon-blue); margin-bottom: 10px; font-size: 1.1rem;">⚙️ Vote Rules</h3>
<?php foreach ($VOTE_RULES as $min_words => $weight): ?>
<p style="margin: 5px 0; font-size: 0.95rem;">
<span style="color: var(--neon-yellow);">≥ <?php echo $min_words; ?> words</span>
→
<span style="color: var(--neon-green); font-weight: bold;"><?php echo $weight / 100; ?>%</span>
</p>
<?php endforeach; ?>
</div>
</div>
<?php if (count($eligible_posts) > 0): ?>
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2 style="margin: 0;">✅ Eligible Posts (<?php echo count($eligible_posts); ?>)</h2>
<button onclick="voteAll()" style="padding: 10px 20px;">🚀 Vote All</button>
</div>
<table>
<thead>
<tr>
<th>Author</th>
<th>Title</th>
<th>Words</th>
<th>Weight</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<?php foreach ($eligible_posts as $p):
$words = count_words($p['body']);
$vote_weight = get_vote_weight($words);
?>
<tr>
<td>
<a href="https://peakd.com/@<?php echo $p['author']; ?>"
target="_blank"
style="color: var(--neon-blue); text-decoration: none;">
@<?php echo $p['author']; ?>
</a>
</td>
<td>
<a href="https://peakd.com/@<?php echo $p['author']; ?>/<?php echo $p['permlink']; ?>"
target="_blank"
style="color: var(--text-primary); text-decoration: none;">
<?php echo htmlspecialchars(mb_substr($p['title'], 0, 60)) . (mb_strlen($p['title']) > 60 ? '...' : ''); ?>
</a>
</td>
<td><?php echo $words; ?></td>
<td>
<span style="
color: <?php
echo $vote_weight >= 600 ? 'var(--neon-green)' :
($vote_weight >= 500 ? 'var(--neon-yellow)' : 'var(--neon-blue)');
?>;
font-weight: bold;
">
<?php echo $vote_weight / 100; ?>%
</span>
</td>
<td>
<button
class="vote-btn"
onclick="votePost(
'<?php echo addslashes($p['author']); ?>',
'<?php echo addslashes($p['permlink']); ?>',
this,
<?php echo $words; ?>
)">
Vote <?php echo $vote_weight / 100; ?>%
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="card">
<h2>⚠️ No Eligible Posts</h2>
<p style="color: var(--text-secondary);">All posts have already been voted on or don't meet criteria.</p>
</div>
<?php endif; ?>
<!-- ========== PATCH 1: Vote History (AMÉLIORÉ) ========== -->
<div id="vote-history" style="
margin-top: 40px;
padding: 30px;
background: linear-gradient(135deg, rgba(18, 18, 48, 0.95), rgba(30, 30, 70, 0.85));
border-radius: 15px;
border: 2px solid var(--neon-purple);
box-shadow: 0 8px 32px rgba(178, 0, 255, 0.3);
">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2 style="
font-family: 'Orbitron', sans-serif;
color: var(--neon-purple);
text-shadow: 0 0 10px var(--neon-purple);
margin: 0;
">📊 Vote Session History</h2>
<button onclick="clearVoteHistory()" style="
background: linear-gradient(45deg, #ff0040, #ff6b9d);
color: white;
border: none;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s;
font-family: 'Orbitron', sans-serif;
" onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">
🗑️ Clear History
</button>
</div>
<div id="history-list" style="
display: grid;
gap: 15px;
max-height: 500px;
overflow-y: auto;
"></div>
</div>
<?php if (DEBUG_MODE): ?>
<div class="debug-table">
<h2>🐞 Debug Log</h2>
<table>
<thead>
<tr>
<th>Author</th>
<th>Permlink</th>
<th>Words</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<?php foreach ($debug_logs as $log): ?>
<tr>
<td>@<?php echo htmlspecialchars($log['author']); ?></td>
<td><?php echo htmlspecialchars($log['permlink']); ?></td>
<td><?php echo $log['words']; ?></td>
<td><?php echo $log['reason']; ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<script>
// ========== CONFIGURATION ==========
const VOTE_STORAGE_KEY = 'hiveVoteHistory';
const ACCOUNT_STORAGE_KEY = 'hiveVoterAccount';
const VOTE_DELAY = 1000;
const AUTO_REFRESH_DELAY = 5000;
// ========== AUTO-VOTE CONFIG (FROM PHP) ==========
const AUTO_VOTE_ENABLED = <?php echo AUTO_VOTE_ENABLED ? 'true' : 'false'; ?>;
const AUTO_VOTE_DELAY = <?php echo AUTO_VOTE_DELAY; ?>;
const AUTO_REFRESH_AFTER_VOTE = <?php echo AUTO_REFRESH_AFTER_VOTE ? 'true' : 'false'; ?>;
// ========== VOTE RULES (SYNCHRONIZED WITH PHP) ==========
const VOTE_RULES = <?php echo json_encode($VOTE_RULES); ?>;
function calculateVoteWeight(wordCount) {
// Trie les seuils par ordre décroissant
const sortedThresholds = Object.keys(VOTE_RULES)
.map(Number)
.sort((a, b) => b - a);
for (const minWords of sortedThresholds) {
if (wordCount >= minWords) {
return VOTE_RULES[minWords];
}
}
return 0;
}
// ========== VOTE SESSION ==========
let voteSession = {
history: [],
votedPosts: []
};
let autoRefreshTimer = null;
// ========== CONNECT ACCOUNT ==========
function connectAccount() {
const accountInput = document.getElementById('account-input');
const statusDiv = document.getElementById('account-status');
const username = accountInput.value.trim().replace('@', '');
if (!username) {
alert('⚠️ Please enter a username');
return;
}
statusDiv.style.display = 'block';
statusDiv.className = 'status-badge status-loading';
statusDiv.textContent = '⏳ Validating account...';
fetch('https://api.hive.blog', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'condenser_api.get_accounts',
params: [[username]],
id: 1
})
})
.then(res => res.json())
.then(data => {
if (data.result && data.result.length > 0) {
localStorage.setItem(ACCOUNT_STORAGE_KEY, username);
document.getElementById('current-account-display').textContent = `@${username}`;
statusDiv.className = 'status-badge status-success';
statusDiv.textContent = `✅ Connected as @${username}`;
console.log(`✅ Account validated: @${username}`);
} else {
statusDiv.className = 'status-badge status-failed';
statusDiv.textContent = '❌ Account not found';
}
})
.catch(err => {
statusDiv.className = 'status-badge status-failed';
statusDiv.textContent = '❌ Connection error';
console.error(err);
});
}
// ========== VOTE FUNCTION ==========
function votePost(author, permlink, button, wordCount) {
const savedAccount = localStorage.getItem(ACCOUNT_STORAGE_KEY) || '<?php echo VOTER_ACCOUNT; ?>';
if (button.disabled) return;
button.disabled = true;
button.textContent = "⏳ Voting...";
// Calculate weight using centralized function
let weight = calculateVoteWeight(wordCount);
console.log(`🗳️ Voting for @${author}/${permlink} with ${weight/100}% (${wordCount} words)`);
hive_keychain.requestVote(
savedAccount,
permlink,
author,
weight,
response => {
if (response.success) {
button.textContent = "✅ Voted";
button.classList.add("voted");
addVoteToSession(author, permlink, weight, true);
updateHistoryDisplay();
console.log(`✅ Successfully voted for @${author}/${permlink}`);
} else {
button.textContent = "❌ Failed";
button.style.background = "linear-gradient(90deg, var(--neon-red), #ff7a18)";
console.error("Vote error:", response.message);
alert(`❌ Vote failed: ${response.message}`);
}
}
);
}
// ========== ADD VOTE TO SESSION ==========
function addVoteToSession(author, permlink, weight, success) {
const voteData = {
author: author,
permlink: permlink,
weight: weight,
success: success,
timestamp: Date.now()
};
voteSession.history.unshift(voteData);
voteSession.votedPosts.push(`${author}/${permlink}`);
localStorage.setItem(VOTE_STORAGE_KEY, JSON.stringify(voteSession));
}
// ========== LOAD VOTE SESSION ==========
function loadVoteSession() {
const stored = localStorage.getItem(VOTE_STORAGE_KEY);
if (stored) {
voteSession = JSON.parse(stored);
updateHistoryDisplay();
}
}
// ========== CLEAR VOTE HISTORY ==========
function clearVoteHistory() {
if (confirm('🗑️ Clear all vote history?')) {
voteSession = { history: [], votedPosts: [] };
localStorage.removeItem(VOTE_STORAGE_KEY);
updateHistoryDisplay();
console.log('✅ Vote history cleared');
}
}
// ========== UPDATE HISTORY DISPLAY ==========
function updateHistoryDisplay() {
const historyList = document.getElementById('history-list');
if (voteSession.history.length === 0) {
historyList.innerHTML = '<p style="text-align: center; color: var(--text-secondary); padding: 20px;">No votes yet in this session</p>';
return;
}
historyList.innerHTML = voteSession.history.map(vote => {
const authorName = `@${vote.author}`;
const postUrl = `https://peakd.com/@${vote.author}/${vote.permlink}`;
const profileUrl = `https://peakd.com/@${vote.author}`;
const avatarUrl = `https://images.hive.blog/u/${vote.author}/avatar`;
const timeAgo = getTimeAgo(vote.timestamp);
return `
<div class="history-item">
<a href="${profileUrl}" target="_blank">
<img src="${avatarUrl}" alt="${authorName}" class="history-avatar"
onerror="this.src='https://images.hive.blog/u/hive.blog/avatar'">
</a>
<div class="history-content">
<a href="${profileUrl}" target="_blank" class="history-author">
${authorName}
</a>
<a href="${postUrl}" target="_blank" class="history-post-link">
🔗 View post on PeakD
</a>
</div>
<div class="history-meta">
<span class="history-weight">💎 ${vote.weight / 100}%</span>
<span class="history-time">${timeAgo}</span>
</div>
</div>
`;
}).join('');
}
function getTimeAgo(timestamp) {
const now = Date.now();
const diffMs = now - timestamp;
const diffMins = Math.floor(diffMs / 60000);
if (diffMins < 1) return 'Just now';
if (diffMins < 60) return `${diffMins}m ago`;
const diffHours = Math.floor(diffMins / 60);
if (diffHours < 24) return `${diffHours}h ago`;
const diffDays = Math.floor(diffHours / 24);
return `${diffDays}d ago`;
}
// ========== AUTO-VOTE FUNCTION ==========
function startAutoVote() {
const buttons = document.querySelectorAll("button.vote-btn:not([disabled])");
if (buttons.length === 0) {
console.log("✅ No posts to vote - scheduling refresh");
if (AUTO_REFRESH_AFTER_VOTE) {
scheduleAutoRefresh();
}
return;
}
console.log(`🤖 AUTO-VOTE: Starting for ${buttons.length} posts`);
showAutoVoteIndicator(buttons.length);
let index = 0;
function voteNext() {
if (index >= buttons.length) {
console.log("✅ AUTO-VOTE: Complete");
hideAutoVoteIndicator();
if (AUTO_REFRESH_AFTER_VOTE) {
scheduleAutoRefresh();
}
return;
}
updateAutoVoteIndicator(index + 1, buttons.length);
buttons[index].click();
index++;
setTimeout(voteNext, AUTO_VOTE_DELAY);
}
voteNext();
}
// ========== VOTE ALL FUNCTION (MANUAL) ==========
function voteAll() {
const buttons = document.querySelectorAll("button.vote-btn:not([disabled])");
if (buttons.length === 0) {
console.log("✅ No eligible posts - refreshing in 5 seconds");
scheduleAutoRefresh();
return;
}
let index = 0;
function voteNext() {
if (index >= buttons.length) {
console.log("✅ Voting complete - refreshing in 5 seconds");
scheduleAutoRefresh();
return;
}
buttons[index].click();
index++;
setTimeout(voteNext, 3000);
}
voteNext();
}
// ========== AUTO-VOTE INDICATOR ==========
function showAutoVoteIndicator(totalPosts) {
let indicator = document.getElementById('auto-vote-indicator');
if (!indicator) {
indicator = document.createElement('div');
indicator.id = 'auto-vote-indicator';
indicator.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(18, 18, 48, 0.98);
border: 2px solid var(--neon-blue);
border-radius: 15px;
padding: 30px 40px;
z-index: 10000;
text-align: center;
box-shadow: 0 8px 32px rgba(0, 212, 255, 0.5);
animation: pulse-glow 2s infinite;
`;
document.body.appendChild(indicator);
}
indicator.innerHTML = `
<div style="font-size: 3rem; margin-bottom: 15px;">🤖</div>
<div style="font-family: 'Orbitron', sans-serif; font-size: 1.5rem; color: var(--neon-blue); margin-bottom: 10px;">AUTO-VOTE ACTIVE</div>
<div id="auto-vote-progress" style="font-size: 1.2rem; color: var(--neon-green);">0 / ${totalPosts}</div>
`;
indicator.style.display = 'block';
}
function updateAutoVoteIndicator(current, total) {
const progress = document.getElementById('auto-vote-progress');
if (progress) {
progress.textContent = `${current} / ${total}`;
}
}
function hideAutoVoteIndicator() {
const indicator = document.getElementById('auto-vote-indicator');
if (indicator) {
indicator.style.animation = 'fadeOut 0.5s forwards';
setTimeout(() => {
indicator.remove();
}, 500);
}
}
// ========== AUTO-REFRESH FUNCTIONS ==========
function clearAutoRefresh() {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer);
autoRefreshTimer = null;
const timerDisplay = document.getElementById('refresh-timer');
if (timerDisplay) {
timerDisplay.remove();
}
}
}
function scheduleAutoRefresh(delay = AUTO_REFRESH_DELAY) {
clearAutoRefresh();
let remainingTime = delay / 1000;
let timerDisplay = document.getElementById('refresh-timer');
if (!timerDisplay) {
timerDisplay = document.createElement('div');
timerDisplay.id = 'refresh-timer';
timerDisplay.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: rgba(18, 18, 48, 0.95);
border: 1px solid rgba(0, 212, 255, 0.4);
border-radius: 8px;
padding: 12px 20px;
color: var(--neon-blue);
font-family: 'Orbitron', sans-serif;
font-size: 0.9rem;
z-index: 9999;
box-shadow: 0 4px 12px rgba(0, 212, 255, 0.3);
animation: pulse-glow 2s infinite;
`;
document.body.appendChild(timerDisplay);
}
timerDisplay.textContent = `🔄 Refresh in ${remainingTime}s`;
autoRefreshTimer = setInterval(() => {
remainingTime--;
timerDisplay.textContent = `🔄 Refresh in ${remainingTime}s`;
if (remainingTime <= 0) {
clearAutoRefresh();
location.reload();
}
}, 1000);
}
// ========== EXPORT CSV ==========
function exportVotedAccountsCSV() {
if (voteSession.history.length === 0) {
alert('⚠️ No votes to export!');
return;
}
const uniqueAccounts = [...new Set(
voteSession.history.map(vote => vote.author.replace('@', ''))
)];
let csvContent = "Username\n";
csvContent += uniqueAccounts.join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
const now = new Date();
const timestamp = now.toISOString().slice(0, 19).replace(/:/g, '-');
const filename = `hive_voted_accounts_${timestamp}.csv`;
link.setAttribute('href', url);
link.setAttribute('download', filename);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
console.log(`✅ Exported ${uniqueAccounts.length} unique accounts to ${filename}`);
alert(`✅ Exported ${uniqueAccounts.length} accounts!\n\nFilename: ${filename}`);
}
// ========== AUTO-START ON PAGE LOAD ==========
window.addEventListener('DOMContentLoaded', () => {
loadVoteSession();
const savedAccount = localStorage.getItem(ACCOUNT_STORAGE_KEY);
if (savedAccount) {
document.getElementById('current-account-display').textContent = `@${savedAccount}`;
}
const eligibleCount = <?php echo count($eligible_posts); ?>;
// Auto-vote
if (AUTO_VOTE_ENABLED && eligibleCount > 0) {
console.log(`🤖 AUTO-VOTE enabled: ${eligibleCount} posts detected`);
setTimeout(() => {
startAutoVote();
}, 2000); // 2s p
} else if (eligibleCount === 0) {
console.log("⚠️ No eligible posts - scheduling auto-refresh");
scheduleAutoRefresh();
}
// Ajoute le bouton d'export CSV
const historySection = document.getElementById('vote-history');
if (historySection) {
const clearButton = historySection.querySelector('button[onclick="clearVoteHistory()"]');
if (clearButton) {
const exportButton = document.createElement('button');
exportButton.textContent = '📊 Export CSV';
exportButton.onclick = exportVotedAccountsCSV;
exportButton.style.cssText = `
background: linear-gradient(45deg, #00d4ff, #b200ff);
color: white;
border: none;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
margin-right: 10px;
transition: all 0.3s;
font-family: 'Orbitron', sans-serif;
`;
exportButton.onmouseover = () => exportButton.style.transform = 'scale(1.05)';
exportButton.onmouseout = () => exportButton.style.transform = 'scale(1)';
clearButton.parentElement.insertBefore(exportButton, clearButton);
}
}
});
window.addEventListener('beforeunload', () => {
clearAutoRefresh();
});
// Ajoute l'animation fadeOut pour l'indicateur
const style = document.createElement('style');
style.textContent = `
@keyframes fadeOut {
from { opacity: 1; transform: translate(-50%, -50%) scale(1); }
to { opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
}
`;
document.head.appendChild(style);
</script>
</body>
</html>
Now we can change the voting conditions directly at the top of the code, it perfectly tracks the number of words based on the peakd counter normally, but you can change it to ecency or hive.blog in the code.

You can connect directly to the robot via the keychain.
The page refreshes every 5 seconds if it finds nothing to vote on; if it finds something, it votes sequentially every 5 seconds, and refreshes after the votes are cast. No further errors are detected.

It only scans the last 20 posts; the API I'm using doesn't go back any further.


It records the last 1000 votes, then you have to manually "clear" the list
You can export the list of people who voted before "clearing".
I simply forgot to put the play/pause button back in. Here's the latest code for this option to add just before your last < /body > tag at the bottom of the code.
<script>
// ========== AUTO-VOTE TOGGLE FUNCTION ==========
let autoVotePaused = false;
let autoVoteInterval = null;
function toggleAutoVote() {
autoVotePaused = !autoVotePaused;
const toggleBtn = document.getElementById('autoVoteToggle');
const statusText = document.getElementById('autoVoteStatus');
if (autoVotePaused) {
toggleBtn.textContent = "▶";
statusText.textContent = "PAUSED";
statusText.style.color = "var(--neon-red)";
clearAutoRefresh();
clearInterval(autoVoteInterval);
// Arrêter le processus auto-vote en cours
if (window.voteNextTimeout) {
clearTimeout(window.voteNextTimeout);
}
} else {
toggleBtn.textContent = "⏸";
statusText.textContent = "ACTIVE";
statusText.style.color = "var(--neon-green)";
// Relancer l'auto-vote si des posts sont éligibles
const eligibleCount = <?php echo count($eligible_posts); ?>;
if (eligibleCount > 0) {
setTimeout(() => {
startAutoVote();
}, 1000);
}
}
}
// PAY PAUSE
window.startAutoVoteOriginal = window.startAutoVote;
window.startAutoVote = function() {
if (autoVotePaused) return;
window.startAutoVoteOriginal();
}
// Initialisation
document.addEventListener('DOMContentLoaded', function() {
// Vérifier si l'auto-vote est activé dans la config
if (!<?php echo AUTO_VOTE_ENABLED ? 'true' : 'false'; ?>) {
const toggleBtn = document.getElementById('autoVoteToggle');
const statusText = document.getElementById('autoVoteStatus');
toggleBtn.textContent = "▶";
statusText.textContent = "DISABLED";
statusText.style.color = "var(--text-secondary)";
toggleBtn.onclick = function() {
alert("Auto-vote is disabled in configuration. Set AUTO_VOTE_ENABLED to true in PHP config.");
};
}
});
</script>

I still can't get it to automatically send a token, grrrrr