code auto-vote 500 words + vote all 8 minute functional, has improved a bit

image.png

For anyone who would like to use this as a starting point for their own project.

seprateurdetexte-ezgif.com-static-to-gif-converter.gif

How Hive Voting Power Works

Voting Power—also called Voting Mana—is the energy used when you upvote or downvote a post or comment. It gradually regenerates at 20% per day, so a full recharge takes about five days. That works out to roughly 1 percentage point every 72 minutes:

[
5 \times 24 \times 60 = 7{,}200 \text{ minutes}
]

[
7{,}200 \div 100 = 72 \text{ minutes per percentage point}
]

A vote cast at 100% voting strength uses 1/50 of your remaining Voting Mana—about 2%. A vote cast at a lower strength uses proportionally less mana. For example, a vote at 5% strength uses about one-twentieth as much mana as a 100% vote.

Example: If your Voting Power is at 89%, it needs to recover 11 percentage points to reach 100%. With no further votes, that would take approximately 13 hours and 12 minutes. Any new votes during that time consume mana and affect the recovery estimate.

Your Hive Power (HP) influences the value of your vote; Voting Power is the mana reserve used to cast it. (hive.blog)

seprateurdetexte-ezgif.com-static-to-gif-converter.gif

What are Resource Credits (RC)?

Resource Credits (RC) are used to pay for the computational and network resources required to perform actions on the Hive blockchain—such as posting, commenting, voting, or transferring tokens. They are separate from Voting Power: Voting Power is spent when you cast a vote, while RC is spent to process the blockchain transaction that records it.

RC is not a token fee that gets sent to someone else. Each account has an RC reserve, which regenerates over time, generally over a five-day period. The account’s available RC is linked to its Hive Power: more HP generally means a larger RC capacity. If an account doesn’t have enough RC, it may be unable to submit a transaction until some RC regenerates. The exact RC cost can vary depending on the resources required by the action and network conditions. (developers.hive.io)


<?php
// Hive voter: server-side initial inventory + client-side Keychain voting.
define('VOTER_ACCOUNT', 'we-are-ai');
define('MIN_WORD_COUNT', 500);
define('POSTS_PER_PAGE', 20);
define('MIN_VP_THRESHOLD', 80);
define('API_URL', 'https://api.hive.blog');
define('API_NODES', ['https://api.hive.blog', 'https://api.deathwing.me', 'https://api.openhive.network']);
$VOTE_RULES = [500 => 500]; // basis points: 500 = 5%

function hive_rpc(string $method, array $params = []) {
    foreach (API_NODES as $node) {
        $ch = curl_init($node);
        curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_POST=>true,
            CURLOPT_HTTPHEADER=>['Content-Type: application/json'],
            CURLOPT_POSTFIELDS=>json_encode(['jsonrpc'=>'2.0','method'=>$method,'params'=>$params,'id'=>1]),
            CURLOPT_CONNECTTIMEOUT=>8, CURLOPT_TIMEOUT=>15, CURLOPT_SSL_VERIFYPEER=>true]);
        $raw=curl_exec($ch); $err=curl_error($ch); curl_close($ch);
        $data=is_string($raw)?json_decode($raw,true):null;
        if (!$err && is_array($data) && !isset($data['error']) && isset($data['result']) && is_array($data['result'])) return $data['result'];
    }
    return [];
}

function count_words(string $text): int {
    $text = preg_replace('/^#+\s*.+$/m', ' ', $text);
    $text = strip_tags($text);
    $text = preg_replace('/https?:\/\/\S+|[@#][\w.-]+/u', ' ', $text);
    $text = trim(preg_replace('/\s+/u', ' ', $text));
    return $text === '' ? 0 : count(preg_split('/\s+/u', $text));
}

function vote_weight(int $words, array $rules): int {
    uksort($rules, fn($a,$b) => (int)$b <=> (int)$a);
    foreach ($rules as $minimum => $weight) if ($words >= (int)$minimum) return (int)$weight;
    return 0;
}

$posts = hive_rpc('bridge.get_ranked_posts', ['sort'=>'created','tag'=>'','observer'=>VOTER_ACCOUNT,'limit'=>POSTS_PER_PAGE]);
$initial_posts = [];
foreach (is_array($posts) ? $posts : [] as $post) {
    $words = count_words((string)($post['body'] ?? ''));
    $voted = false;
    foreach (($post['active_votes'] ?? []) as $vote) if (($vote['voter'] ?? '') === VOTER_ACCOUNT) { $voted = true; break; }
    if (!$voted && $words >= MIN_WORD_COUNT && vote_weight($words, $VOTE_RULES) > 0) {
        $initial_posts[] = [
            'author'=>(string)($post['author'] ?? ''), 'permlink'=>(string)($post['permlink'] ?? ''),
            'title'=>(string)($post['title'] ?? ''), 'body'=>(string)($post['body'] ?? ''),
            'created'=>(string)($post['created'] ?? ''), 'words'=>$words
        ];
    }
}
?><!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Hive voter</title>
<style>
:root{--bg:#09111f;--card:#111d31;--line:#263956;--text:#e8f0ff;--muted:#9eb0cb;--blue:#55c7ff;--green:#58e39b;--red:#ff718b;--yellow:#ffd166}*{box-sizing:border-box}body{margin:0;background:linear-gradient(140deg,#07101d,#101b32);color:var(--text);font:15px system-ui,sans-serif;padding:24px}main{max-width:1180px;margin:auto}h1{margin:0 0 20px;color:var(--blue)}h2{margin:0 0 14px;font-size:19px}.card{background:rgba(17,29,49,.94);border:1px solid var(--line);border-radius:12px;padding:18px;margin:0 0 18px;box-shadow:0 10px 30px #0003}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:14px}.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}input{background:#0a1527;color:var(--text);border:1px solid var(--line);border-radius:7px;padding:8px;max-width:130px}button{background:#176a9a;color:#fff;border:0;border-radius:7px;padding:9px 13px;cursor:pointer}button:hover{filter:brightness(1.15)}button:disabled{opacity:.55;cursor:not-allowed}button.danger{background:#8c2946}button.pause{background:#74551b}a{color:var(--blue)}table{width:100%;border-collapse:collapse}th,td{text-align:left;padding:10px 8px;border-bottom:1px solid var(--line);vertical-align:middle}th{color:var(--muted);font-size:12px;text-transform:uppercase}.scroll{overflow:auto}.bar{height:12px;background:#07101d;border-radius:20px;overflow:hidden;margin-top:8px}.fill{height:100%;background:linear-gradient(90deg,var(--green),var(--blue));width:0;transition:width .3s}.rule{display:flex;gap:7px;align-items:center;margin:7px 0}.rule input{width:90px}.status{color:var(--muted)}.ok{color:var(--green)}.warn{color:var(--yellow)}.bad{color:var(--red)}#history-list{display:grid;gap:7px}.history{padding:9px;border:1px solid var(--line);border-radius:7px;display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap}.small{font-size:13px;color:var(--muted)}@media(max-width:650px){body{padding:12px}th,td{padding:8px 4px;font-size:13px}.hide-mobile{display:none}}
</style></head><body><main>
<h1>Hive voter</h1>
<section class="card"><h2>Connection and status</h2><div class="grid"><div><div class="row"><input id="account-input" placeholder="Hive account"><button id="connect-btn" type="button">Connect / Switch</button></div><div id="account-status" class="status" aria-live="polite"></div><p>Active account: <strong id="current-account"><?=htmlspecialchars(VOTER_ACCOUNT)?></strong></p></div><div><div>VP: <strong id="vp-value">—</strong>% <span id="vp-state" class="status"></span></div><div class="bar"><div id="vp-bar" class="fill"></div></div><div class="small">Threshold: <input id="vp-threshold" type="number" min="0" max="100" step="1">% · refreshes every 30 s</div></div><div><div>Automation: <strong id="auto-state">active</strong></div><button id="pause-btn" class="pause" type="button">Pause</button><div id="countdown" class="small">Next vote: ready</div><div class="small">Persistent queue: <span id="queue-count">0</span></div></div></div></section>
<section class="card"><h2>Weight rules</h2><div id="rules"></div><button id="add-rule" type="button">Add tier</button><p class="small">Weight in percentage, per word tier. Default: 5% from 500 words.</p></section>
<section class="card"><div class="row" style="justify-content:space-between"><h2>Eligible posts</h2><span id="post-status" class="status">Loading…</span></div><div class="scroll"><table><thead><tr><th>Author</th><th>Post</th><th>Words</th><th>Weight</th><th>Action</th></tr></thead><tbody id="posts-body"></tbody></table></div></section>
<section class="card"><div class="row" style="justify-content:space-between"><h2>Vote history</h2><button id="clear-history" class="danger" type="button">Clear</button></div><div id="history-list"></div></section>
</main>
<script>
'use strict';
const API_NODES=['https://api.hive.blog','https://api.deathwing.me','https://api.openhive.network'], MIN_WORDS=<?=MIN_WORD_COUNT?>, POSTS_PER_PAGE=<?=POSTS_PER_PAGE?>, VOTE_DELAY=480000, SCAN_INTERVAL=10000, VP_REFRESH_INTERVAL=30000;
const INITIAL_POSTS=<?=json_encode($initial_posts, JSON_HEX_TAG|JSON_HEX_APOS|JSON_HEX_QUOT|JSON_HEX_AMP)?>;
const KEYS={account:'hiveVoterAccount',rules:'hiveVoterRules',threshold:'hiveVoterVpThreshold',queue:'hiveVoterQueue',last:'hiveVoterLastVoteAt',history:'hiveVoterHistory',paused:'hiveVoterPaused',pending:'hiveVoterPending'};
const $=id=>document.getElementById(id); const safe=(fn,...args)=>{try{return fn(...args)}catch(e){return null}};
let rules=[], queue=[], history=[], pending=new Set(), paused=localStorage.getItem(KEYS.paused)==='1', currentVP=0, inFlight=false, scanBusy=false, countdownTimer=null, scanTimer=null, vpTimer=null;
function readJSON(key,fallback){try{const v=JSON.parse(localStorage.getItem(key));return v??fallback}catch(e){return fallback}}
function save(key,value){localStorage.setItem(key,JSON.stringify(value))}
function postKey(p){return String(p.author)+'/'+String(p.permlink)}
function countWords(text){const clean=String(text||'').replace(/^#+\s*.+$/gm,' ').replace(/<[^>]*>/g,' ').replace(/https?:\/\/\S+|[@#][\w.-]+/gu,' ').trim();return clean?clean.split(/\s+/u).length:0}
function weightFor(words){return rules.slice().sort((a,b)=>b.words-a.words).find(r=>words>=r.words)?.weight||0}
function renderRules(){const box=$('rules');if(!box)return;box.innerHTML='';rules.slice().sort((a,b)=>a.words-b.words).forEach((r,i)=>{const row=document.createElement('div');row.className='rule';row.innerHTML='<label>≥ <input type="number" min="1" step="1" class="rw"> words</label><label><input type="number" min="0" max="100" step="0.1" class="rp"> %</label><button type="button" class="danger">Remove</button>';row.querySelector('.rw').value=r.words;row.querySelector('.rp').value=r.weight;row.querySelector('.rw').oninput=e=>{r.words=Math.max(1,Number(e.target.value)||1);save(KEYS.rules,rules);refreshTable()};row.querySelector('.rp').oninput=e=>{r.weight=Math.max(0,Math.min(100,Number(e.target.value)||0));save(KEYS.rules,rules);refreshTable()};row.querySelector('button').onclick=()=>{rules=rules.filter(x=>x!==r);if(!rules.length)rules=[{words:500,weight:5}];save(KEYS.rules,rules);renderRules();refreshTable()};box.appendChild(row)})}
function makeRow(p){const w=Number(p.words)||countWords(p.body), weight=weightFor(w);if(w<MIN_WORDS||weight<=0)return null;const tr=document.createElement('tr');const author=document.createElement('a');author.href='https://peakd.com/@'+encodeURIComponent(p.author);author.target='_blank';author.rel='noopener';author.textContent='@'+p.author;const title=document.createElement('a');title.href='https://peakd.com/@'+encodeURIComponent(p.author)+'/'+encodeURIComponent(p.permlink);title.target='_blank';title.rel='noopener';title.textContent=(p.title||p.permlink||'').slice(0,80);const a=document.createElement('td'),b=document.createElement('td'),c=document.createElement('td'),d=document.createElement('td'),e=document.createElement('td');a.appendChild(author);b.appendChild(title);c.textContent=String(w);d.textContent=weight.toFixed(1)+'%';const key=postKey(p);const btn=document.createElement('button');btn.type='button';btn.className='vote-btn';btn.dataset.key=key;btn.textContent=pending.has(key)?'In progress...':'Vote '+weight.toFixed(1)+'%';btn.disabled=pending.has(key);btn.onclick=()=>votePost(p.author,p.permlink,btn,w);e.appendChild(btn);[a,b,c,d,e].forEach(x=>tr.appendChild(x));tr.dataset.key=key;return tr}
function refreshTable(){const body=$('posts-body');if(!body)return;const seen=new Set();body.querySelectorAll('tr').forEach(tr=>seen.add(tr.dataset.key));queue.forEach(p=>{if(!seen.has(postKey(p))){const row=makeRow(p);if(row)body.appendChild(row)}});const status=$('post-status'), count=$('queue-count');if(status)status.textContent=queue.length+' post(s) in queue';if(count)count.textContent=queue.length}
function addToQueue(posts){let changed=false;const keys=new Set(queue.map(postKey));posts.forEach(p=>{p.words=Number(p.words)||countWords(p.body);if(p.words>=MIN_WORDS&&weightFor(p.words)>0&&!keys.has(postKey(p))&&!pending.has(postKey(p))){queue.push(p);keys.add(postKey(p));changed=true}});queue.sort((a,b)=>new Date(a.created||0)-new Date(b.created||0));if(changed){save(KEYS.queue,queue);refreshTable()}return changed}
async function rpc(method,params){let lastError=null;for(const endpoint of API_NODES){try{const res=await fetch(endpoint,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({jsonrpc:'2.0',method,params,id:1})});if(!res.ok)throw Error(endpoint+' HTTP '+res.status);const data=await res.json();if(data.error)throw Error(endpoint+' '+(data.error.message||'RPC error'));if(Array.isArray(data.result))return data.result;throw Error(endpoint+' invalid RPC response')}catch(e){lastError=e;console.warn('[Hive RPC] failed',method,endpoint,e.message)}}throw lastError||Error('No Hive nodes available')}
function account(){return (localStorage.getItem(KEYS.account)||'<?=VOTER_ACCOUNT?>').replace(/^@/,'').trim()}
async function connectAccount(){const input=$('account-input'), status=$('account-status');const name=(input?.value||'').replace(/^@/,'').trim();if(!name){if(status)status.textContent='Enter an account name.';return}if(status)status.textContent='Validating…';try{const found=await rpc('condenser_api.get_accounts',[[name]]);if(!found.length)throw Error('Account not found');localStorage.setItem(KEYS.account,name);const current=$('current-account');if(current)current.textContent='@'+name;if(status)status.textContent='Account connected.';await scan(true);await refreshVP()}catch(e){if(status)status.textContent='Connection failed.'}}
function canVoteWithVP(vp){const threshold=Number(localStorage.getItem(KEYS.threshold)||80);return Number(vp)>=threshold}
async function refreshVP(){
  try{
    const ac=(await rpc('condenser_api.get_accounts',[[account()]]))[0];
    if(!ac)throw Error('Hive account not found');
    const regen=432000;
    let vp;
    const mb=ac.voting_manabar;
    if(mb&&mb.current_mana!==undefined&&mb.last_update_time!==undefined){
      const vests=x=>Number.parseFloat(String(x||'0').replace(/[^0-9.+-eE]/g,''))||0;
      const effectiveVests=Math.max(0,vests(ac.vesting_shares)+vests(ac.received_vesting_shares)-vests(ac.delegated_vesting_shares));
      const maxMana=Number(mb.max_mana)>0?Number(mb.max_mana):effectiveVests*1000000;
      const now=Math.floor(Date.now()/1000), last=Number(mb.last_update_time)||now;
      const elapsed=Math.max(0,now-last);
      const current=Number(mb.current_mana)||0;
      const regenerated=Math.min(maxMana,Math.max(0,elapsed)*maxMana/regen);
      vp=maxMana>0?Math.max(0,Math.min(100,(current+regenerated)*100/maxMana)):null;
    }
    if(!Number.isFinite(vp)){
      const lastVote=Date.parse(ac.last_vote_time||'');
      const elapsed=Math.max(0,(Date.now()-(Number.isFinite(lastVote)?lastVote:Date.now()))/1000);
      vp=(Number(ac.voting_power)+elapsed*10000/regen)/100;
    }
    if(!Number.isFinite(vp))throw Error('VP is not numeric');
    currentVP=Math.max(0,Math.min(100,vp));
    const v=currentVP;
    const value=$('vp-value'), bar=$('vp-bar'), state=$('vp-state');
    if(value)value.textContent=v.toFixed(2);
    if(bar)bar.style.width=v+'%';
    if(state){
      const threshold=Number(localStorage.getItem(KEYS.threshold)||80);
      const secondsToThreshold=v>=threshold?0:Math.ceil((threshold-v)*regen/100);
      const eta=secondsToThreshold?` · 80% in ${formatDuration(secondsToThreshold)}`:' · ready';
      state.textContent=(v>=threshold?'· ready': '· below threshold')+eta;
      state.className=v>=threshold?'ok':'bad';
    }
  }catch(e){
    console.warn('[Hive VP] unavailable:',e&&e.message||e);
    const state=$('vp-state');if(state){state.textContent='· unavailable';state.className='status'}
  }
}
function votePost(author,permlink,button,wordCount){if(inFlight||!button||button.disabled)return false;const w=weightFor(Number(wordCount));if(w<=0||Number(wordCount)<MIN_WORDS||!canVoteWithVP(currentVP)){button.textContent=canVoteWithVP(currentVP)?'Invalid rule':'Insufficient VP';return false}const key=author+'/'+permlink;if(pending.has(key))return false;pending.add(key);inFlight=true;button.disabled=true;button.textContent='In progress...';const kc=window.hive_keychain;if(!kc||typeof kc.requestVote!=='function'){inFlight=false;pending.delete(key);button.disabled=false;button.textContent='Vote '+w.toFixed(1)+'%';return false}safe(kc.requestVote.bind(kc),account(),permlink,author,Math.round(w*100),response=>{const ok=!!(response&&(response.success===true||response.result!==undefined));if(ok){localStorage.setItem(KEYS.last,String(Date.now()));history.unshift({author,permlink,weight:w,time:Date.now()});save(KEYS.history,history);queue=queue.filter(p=>postKey(p)!==key);save(KEYS.queue,queue);pending.delete(key);inFlight=false;const row=document.querySelector(`tr[data-key="${key}"]`);if(row)row.remove();renderHistory();refreshTable();updateCountdown();}else{pending.delete(key);inFlight=false;button.disabled=false;button.textContent='Vote '+w.toFixed(1)+'%'}});return true}
function formatDuration(seconds){const s=Math.max(0,Math.ceil(Number(seconds)||0)),h=Math.floor(s/3600),m=Math.floor((s%3600)/60),sec=s%60;return (h?h+' h ':'')+(m?m+' min ':'')+sec+' s'}
function renderHistory(){const box=$('history-list');if(!box)return;box.innerHTML=history.length?'':'<div class="small">No votes recorded yet.</div>';history.forEach(v=>{const item=document.createElement('div');item.className='history';const link=document.createElement('a');link.href='https://peakd.com/@'+encodeURIComponent(v.author)+'/'+encodeURIComponent(v.permlink);link.target='_blank';link.rel='noopener';link.textContent='@'+v.author+'/'+v.permlink;item.append(link,document.createTextNode(v.weight.toFixed(1)+'% · '+new Date(v.time).toLocaleString()));box.appendChild(item)})}
function updateCountdown(){
  const last=Number(localStorage.getItem(KEYS.last)||0);
  const due=last?last+VOTE_DELAY:0;
  const remain=Math.max(0,due-Date.now());
  const el=$('countdown');
  if(el)el.textContent=remain?'Next vote: '+formatDuration(Math.ceil(remain/1000)):'Next vote: ready';
}
function startCountdown(){
  clearTimeout(countdownTimer);
  const updateDisplay=()=>{
    updateCountdown();
    const last=Number(localStorage.getItem(KEYS.last)||0);
    const due=last?last+VOTE_DELAY:0;
    const remain=Math.max(0,due-Date.now());
    if(remain>0){
      countdownTimer=setTimeout(updateDisplay,1000);
    } else {
      if(!paused&&canVoteWithVP(currentVP)&&queue.length>0){
        tryVoteNext();
      }
      countdownTimer=setTimeout(updateDisplay,1000);
    }
  };
  updateDisplay();
}
function tryVoteNext(){
  if(paused||inFlight||!queue.length)return;
  const last=Number(localStorage.getItem(KEYS.last)||0);
  if(Date.now()-last<VOTE_DELAY)return;
  const p=queue[0];
  const key=postKey(p);
  if(pending.has(key)){
    console.info('[Hive] Post already being voted, waiting...');
    return;
  }
  const row=[...document.querySelectorAll('#posts-body tr')].find(x=>x.dataset.key===key);
  const btn=row?.querySelector('.vote-btn');
  if(btn){
    votePost(p.author,p.permlink,btn,p.words);
  } else {
    queue.shift();
    save(KEYS.queue,queue);
    refreshTable();
    tryVoteNext();
  }
}
async function scan(initial=false){if(scanBusy){return}scanBusy=true;try{let found=[];let source='condenser_api.get_discussions_by_created';try{found=await rpc(source,[{tag:'',limit:POSTS_PER_PAGE}])}catch(firstError){source='bridge.get_ranked_posts';found=await rpc(source,{sort:'created',tag:'',observer:account(),limit:POSTS_PER_PAGE})}const stats={received:Array.isArray(found)?found.length:0,short:0,voted:0,noWeight:0,accepted:0};const eligible=[];for(const p of (Array.isArray(found)?found:[])){const w=countWords(p.body||'');const voted=(p.active_votes||[]).some(v=>v&&v.voter===account());if(voted){stats.voted++;continue}if(w<MIN_WORDS){stats.short++;continue}if(weightFor(w)<=0){stats.noWeight++;continue}stats.accepted++;eligible.push({...p,words:w})}if(!initial)console.info('[Hive silent scan]','accepted:',stats.accepted);addToQueue(eligible);if(initial)refreshTable()}catch(e){if(initial)console.error('[Hive scan]',e)}finally{scanBusy=false}}
function setPaused(value){try{paused=value;localStorage.setItem(KEYS.paused,paused?'1':'0');const pause=$('pause-btn'),state=$('auto-state');if(pause)pause.textContent=paused?'Resume':'Pause';if(state)state.textContent=paused?'paused':'active'}catch(e){console.warn('[Hive pause]',e)}}
function init(){
  try{
    rules=readJSON(KEYS.rules,[{words:500,weight:5}]);if(!Array.isArray(rules)||!rules.length)rules=[{words:500,weight:5}];
    queue=readJSON(KEYS.queue,[]);history=readJSON(KEYS.history,[]);if(!Array.isArray(queue))queue=[];if(!Array.isArray(history))history=[];
    let threshold=Number(localStorage.getItem(KEYS.threshold)||80);if(!Number.isFinite(threshold))threshold=80;localStorage.setItem(KEYS.threshold,String(threshold));
    const thresholdEl=$('vp-threshold');if(thresholdEl)thresholdEl.value=threshold;
    renderRules();renderHistory();addToQueue(INITIAL_POSTS);refreshTable();setPaused(paused);
    const connect=$('connect-btn');if(connect)connect.onclick=connectAccount;
    const add=$('add-rule');if(add)add.onclick=()=>{rules.push({words:500,weight:5});save(KEYS.rules,rules);renderRules();refreshTable()};
    const clear=$('clear-history');if(clear)clear.onclick=()=>{history=[];save(KEYS.history,history);renderHistory()};
    const pause=$('pause-btn');if(pause)pause.onclick=()=>setPaused(!paused);
    if(thresholdEl)thresholdEl.oninput=e=>{const v=Math.max(0,Math.min(100,Number(e.target.value)||0));localStorage.setItem(KEYS.threshold,String(v));refreshVP()};
    startCountdown();
    vpTimer=setInterval(()=>{try{refreshVP()}catch(e){}},VP_REFRESH_INTERVAL);
    scanTimer=setInterval(()=>{try{scan(false)}catch(e){}},SCAN_INTERVAL);
    try{scan(true)}catch(e){}
    try{refreshVP()}catch(e){}
  }catch(e){
    console.error('[Hive init] bootstrap error:',e);
  }
}
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init);else init();
</script></body></html>

seprateurdetexte-ezgif.com-static-to-gif-converter.gif

Hive Voter: A Complete Explanation of How the Code Works

This code creates a web page that finds Hive posts matching certain criteria, adds them to a queue, and can request votes on them using Hive Keychain.

By default, it looks for posts with at least 500 words and assigns them a 5% vote. It also displays estimated voting power (VP) and prevents votes when VP is below the selected threshold, which defaults to 80%.

Important: the code does not vote directly from the server. The server retrieves an initial list of posts; votes are requested in the browser through Hive Keychain.


1. Default settings

At the beginning of the PHP file, several constants define the initial behavior:

define('VOTER_ACCOUNT', 'we-are-ai');
define('MIN_WORD_COUNT', 500);
define('POSTS_PER_PAGE', 20);
define('MIN_VP_THRESHOLD', 80);
  • VOTER_ACCOUNT: the account displayed when the page loads and used to prepare the initial list of posts.
  • MIN_WORD_COUNT: the minimum number of words required for a post to be eligible.
  • POSTS_PER_PAGE: the number of posts retrieved for each search.
  • MIN_VP_THRESHOLD: the VP threshold declared here.

The default voting rule is:

$VOTE_RULES = [500 => 500];

Vote weights are expressed in basis points: 500 means 5%. In practice, the JavaScript in the page uses percentages. The VP threshold used during interactive operation is stored in the browser; its default is also 80%.


2. What the PHP code does

Before sending the page to the browser, the server queries Hive using RPC.

The hive_rpc() function tries several Hive nodes in sequence:

  1. api.hive.blog
  2. api.deathwing.me
  3. api.openhive.network

For each node, it sends a JSON-RPC request using cURL. If a node fails or returns an unusable response, the function tries the next one. If none of the nodes works, it returns an empty array.

The code then requests recent posts using bridge.get_ranked_posts. For each post, it:

  • approximately counts the words;
  • checks whether the we-are-ai account has already voted on it;
  • keeps posts that have not received its vote, have at least 500 words, and have a voting weight greater than zero.

These initial posts are embedded in the HTML page as INITIAL_POSTS. Special characters are escaped during JSON encoding to reduce the risk of injection into the HTML document.

Important: this initial list is prepared for we-are-ai, even though the user can connect a different account from the page. The browser then performs its own searches using the selected account.


3. Word counting and voting rules

The code includes one word-counting function in PHP and another in JavaScript. Both attempt to remove certain content before counting:

  • Markdown heading lines beginning with #;
  • HTML tags;
  • URLs;
  • some elements beginning with @ or #.

The remaining text is split on whitespace. This is therefore an approximate count, not an editorial analysis. The two versions are not perfectly identical, and results may vary depending on the post’s formatting.

Voting rules can be edited on the page. For example:

  • from 500 words: 5%;
  • from 1,000 words: 7%;
  • from 2,000 words: 10%.

For a given post, the code selects the weight for the highest word-count threshold the post meets. The rules and VP threshold are saved in the browser’s localStorage: they persist in that browser, but are not automatically shared with other devices.

Posts under 500 words remain ineligible, even if a custom rule assigns them a weight.


4. What the page displays

The interface is organized into several sections:

Connection and status

The user can enter a Hive account name and click Connect / Switch. The code checks that the account exists before saving it in the browser.

The page also displays:

  • the currently selected account;
  • estimated VP;
  • a VP bar;
  • the VP threshold;
  • automation status;
  • a countdown to the next automatic vote;
  • the number of posts in the queue.

Voting rules

You can add, edit, or remove word-count thresholds and their percentage weights.

Eligible posts

For each displayed post, the table shows:

  • the author;
  • the title;
  • the word count;
  • the calculated weight;
  • a button to request a vote.

The links open the posts on PeakD.

History

After a vote that the code considers successful, the history records the author, permlink, weight, and time. The Clear button deletes this local history.


5. How VP is calculated

The refreshVP() function requests account information from Hive. If voting_manabar data is available, it estimates the current mana by accounting for the time since the last update and regeneration over five days.

If that data is unavailable, it falls back to an estimate based on voting_power and last_vote_time.

The result is then limited to the range from 0 to 100%. VP is refreshed every 30 seconds.

When VP is below the threshold, the page indicates that there is not enough VP and estimates how long it may take to reach the threshold. However, the estimate’s text displays “80%” as a fixed value, even if the user has changed the threshold. That message may therefore not match the selected setting.

This is an estimate based on the RPC data received, not a guarantee that Hive will accept a vote.


6. The queue and periodic searches

When the page loads, the initial posts are added to a queue. The browser then searches for posts approximately every 10 seconds.

For each post found, it checks that:

  1. it meets the minimum word count;
  2. a rule assigns it a weight;
  3. the selected account has not already voted on it;
  4. it is not already in the queue or being processed.

The queue is saved locally in the browser, as is the timestamp of the last vote. Posts are sorted by creation date, with the oldest first.

Each search is limited to 20 recent posts returned by the Hive nodes. The code does not guarantee that it will scan every Hive post or find every post that might meet the criteria.


7. How a vote is submitted

When a vote is requested, the browser calls:

hive_keychain.requestVote(...)

The code does not contain a private key. Hive Keychain must be installed and handles the authorization request and vote signature.

The weight is converted to basis points before it is sent: for example, 5% becomes 500.

The code refuses to submit a vote if:

  • another vote request is already in progress;
  • the post no longer meets the rules;
  • VP is below the threshold;
  • Hive Keychain is unavailable on the page.

If Keychain’s response is interpreted as successful, the code adds the vote to the history, removes the post from the queue, and records the vote time. If the response is interpreted as a failure, the button is re-enabled.


8. Automation, pause, and delay

The delay between automatic votes is set to 480,000 milliseconds, or 8 minutes.

When automation is active, the countdown regularly checks whether a vote can be submitted. If the delay has elapsed, VP meets the threshold, and the queue contains a post, the code attempts to vote on the first post in the queue.

The Pause button prevents automatic voting from starting. It does not change the rules or clear the queue.

Important detail: the eight-minute delay is checked by the automatic voting mechanism. Clicking a vote button manually calls votePost(), but that function does not itself check this delay. VP checks and the handling of an already-running request still apply.


9. Data saved in the browser

The code uses localStorage to save items such as:

  • the selected account;
  • voting rules;
  • the VP threshold;
  • the queue;
  • the time of the last vote;
  • vote history;
  • whether automation is paused.

These data stay in the browser being used. They are not a shared database and can be changed or deleted by the user.

The queue and history are not separated by account. If you switch accounts in the same browser, keep in mind that locally saved items from the previous account may still be there.


10. Limitations and precautions

This code is a voting-assistance tool, not a guarantee that a post will be found or a vote will succeed. A few points are worth keeping in mind:

  • Searches are limited to a small number of recent posts per request.
  • Word counting is approximate.
  • The initial list supplied by PHP is for we-are-ai, even if a different account is connected afterward.
  • The result of a vote depends on Keychain, Hive nodes, and the response received.
  • The history records votes that the code interpreted as successful; it is not a substitute for checking the blockchain.
  • The automatic delay does not apply as a universal restriction to manual votes.
  • The “80%” in the VP estimate remains fixed even if the threshold is changed.
  • The server needs PHP with cURL enabled, and the page must be able to access Hive RPC nodes and Hive Keychain in the browser.

In summary

This program retrieves recent posts, estimates their word counts, applies voting-weight rules, places eligible posts in a local queue, and can request votes through Hive Keychain. It displays estimated VP and attempts an automatic vote no more often than once every eight minutes, provided automation is enabled and the account meets the configured threshold.

The private key is not written into this code: vote signing is delegated to Hive Keychain. The selected account, rules, queue, and history are stored locally in the browser.

#Hive #HiveKeychain #PeakD #Programming #PHP #JavaScript #Blockchain

0.28834081 BEE
2 comments

I have a similar tool I have been wanting to implement certain metrics so it doesn't automatically vote based on the amount of words and the amount of post per day but I have been a little bit busy working on another project @rustedwax going to take a look at yours, I want to implement ke, probably add some Ai prompt to evaluate the content so it holds the automated and ask for human input, not policing but automating part of the evaluation process 🤖✌️

0.00001813 BEE

MagnĂ­fico

0.00000000 BEE