A football predictions site can run entirely on a custom WordPress setup without needing a third-party plugin — a custom database table, a shortcode-driven front end, and a few AJAX handlers are enough to publish daily tips, edit results, and even layer in a paid VIP tier. Here’s how to plan the site and the code that powers a working version of it.
Decide Your Business Model First
Before writing any code, it’s worth locking in how the site will actually make money, since that decision shapes both your content strategy and your technical build. Common approaches for sports prediction sites include display advertising, affiliate commissions from referring users to sportsbooks, subscriptions for premium picks, direct tips sales, and API licensing of prediction data to other platforms — most established sites end up combining more than one of these rather than relying on a single stream.
It’s also worth starting narrow rather than broad. Start focused on one league or competition and expand later once you’ve established authority in one area, rather than trying to cover every league and market from day one.
Why WordPress Is a Reasonable Starting Point
For most new sports content projects, WordPress provides the fastest path to launch, and you can always rebuild on custom infrastructure later if the site outgrows it. There are also existing plugins worth knowing about if you’d rather not build everything from scratch — Football Predictor is built specifically for tournament-style bracket predictions, while the Football Pool plugin handles ongoing pool-style competitions with configurable scoring and joker multipliers. If your goal is closer to a tipster feed than a user-prediction competition, though, a custom build like the one below gives you more control over formatting and monetization.
Building a Custom Tips Feed From Scratch
The approach below creates a dedicated database table for match tips, gives administrators a simple front-end form to post and edit tips without touching wp-admin, and renders the whole feed through a shortcode you can drop into any post or page.
add_action('init',function(){date_default_timezone_set('Africa/Nairobi');global $wpdb;$t=$wpdb->prefix.'matchtips';$c=$wpdb->get_charset_collate();$wpdb->query("CREATE TABLE IF NOT EXISTS $t(id BIGINT AUTO_INCREMENT PRIMARY KEY,matchtip TEXT,pred TEXT,kick VARCHAR(20),result VARCHAR(5) DEFAULT '',created DATETIME DEFAULT CURRENT_TIMESTAMP) $c");$d=date('Y-m-d H:i:s',strtotime('-7 days'));$wpdb->query($wpdb->prepare("DELETE FROM $t WHERE created < %s",$d));});add_action('wp_ajax_mt_add',function(){if(!current_user_can('administrator'))wp_die();global $wpdb;$t=$wpdb->prefix.'matchtips';$tip=sanitize_text_field($_POST['tip']);$pred=sanitize_text_field($_POST['pred']);$kick=sanitize_text_field($_POST['kick']);if(strpos($pred,'@')===false){echo json_encode(['error'=>'Prediction must contain @ odds']);wp_die();}if(strpos($kick,':')===false){echo json_encode(['error'=>'Kickoff must contain :']);wp_die();}$n=$wpdb->get_var("SELECT COALESCE(MIN(a.id+1),1)FROM $t a LEFT JOIN $t b ON a.id+1=b.id WHERE b.id IS NULL");if($wpdb->get_var("SELECT COUNT(*) FROM $t WHERE id=1")=="0")$n=1;$wpdb->query($wpdb->prepare("INSERT INTO $t(id,matchtip,pred,kick,created) VALUES(%d,%s,%s,%s,%s)",$n,$tip,$pred,$kick,date('Y-m-d H:i:s')));$id=$n;echo json_encode(['id'=>$id,'date'=>date('d F Y'),'tip'=>$tip,'pred'=>$pred,'kick'=>$kick]);wp_die();});add_action('wp_ajax_mt_update',function(){if(!current_user_can('administrator'))wp_die();global $wpdb;$t=$wpdb->prefix.'matchtips';$id=intval($_POST['id']);$tip=sanitize_text_field($_POST['tip']);$pred=sanitize_text_field($_POST['pred']);$kick=sanitize_text_field($_POST['kick']);$res=sanitize_text_field($_POST['res']);$wpdb->update($t,['matchtip'=>$tip,'pred'=>$pred,'kick'=>$kick,'result'=>$res],['id'=>$id]);echo json_encode(['tip'=>$tip,'pred'=>$pred,'kick'=>$kick,'res'=>$res]);wp_die();});add_action('wp_ajax_mt_delete',function(){if(!current_user_can('administrator'))wp_die();global $wpdb;$t=$wpdb->prefix.'matchtips';$wpdb->delete($t,['id'=>intval($_POST['id'])]);echo'deleted';wp_die();});add_action('wp_ajax_mt_purge',function(){if(!current_user_can('administrator'))wp_die();wp_update_post(['ID'=>20719]);echo'purged';wp_die();});add_shortcode('match_tips_feed',function(){global $wpdb;$t=$wpdb->prefix.'matchtips';$r=$wpdb->get_results("SELECT * FROM $t ORDER BY DATE(created) DESC,STR_TO_DATE(kick,'%H:%i') DESC");ob_start();?><style>.mtouter{max-width:780px;margin:auto}.topbar{display:flex;justify-content:space-between;margin-bottom:6px}.vipbtn{background:#2271b1;color:#fff;border:0;padding:8px 12px;border-radius:6px;font-weight:600;cursor:pointer}.vipflash{position:relative;overflow:hidden}.vipflash:before{content:"";position:absolute;top:0;left:-75%;width:50%;height:100%;background:linear-gradient(120deg,transparent,rgba(255,255,255,.8),transparent);animation:vipshine 1.8s infinite}@keyframes vipshine{100%{left:125%}}.mtwrap{background:#f8f9fa;padding:10px;border-radius:8px;font-family:Arial}.vipmodal,.mtmodal{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.6);display:none;align-items:center;justify-content:center;z-index:9999}.vipbox{background:#fff;width:340px;padding:18px;border-radius:8px;position:relative}.vipclose{position:absolute;top:6px;right:10px;font-size:20px;cursor:pointer}.vipslots{font-size:12px;color:#b32d2e;margin-bottom:10px;font-weight:600}.vipdonate{display:block;background:#0f7b0f;color:#fff!important;text-align:center;padding:8px;border-radius:6px;margin-bottom:10px;text-decoration:none}.vipbox input,.vipbox textarea{width:100%;margin-bottom:8px;padding:8px;border:1px solid #ccc;border-radius:4px;font-size:13px}.vipbox textarea{min-height:70px;resize:none}.vipbox button{width:100%;background:#2271b1;color:#fff;border:0;padding:9px;border-radius:6px}.mtform input,.mtbox input{width:100%;padding:10px;margin-bottom:8px;border:1px solid #dadce0;border-radius:6px;font-size:14px}.mtbtn{width:100%;background:#1a73e8;color:#fff;border:0;padding:10px;border-radius:6px;font-weight:600;cursor:pointer}.mtpurge{background:#d93025!important;color:#fff!important;margin-bottom:6px}.mtdelete{background:#d93025!important;color:#fff!important;border:none;margin-bottom:6px}.mtstatus{font-size:13px;color:#188038;margin-bottom:10px;display:none}.mtfeed{margin-top:15px}.mtitem{display:grid;grid-template-columns:140px 1fr;background:#fff;border:1px solid #e0e0e0;border-radius:6px;margin-bottom:10px}.mtleft{background:#eef3fd;padding:10px;color:#174ea6;font-weight:600;border-right:2px solid #d2e3fc}.mtright{padding:10px}.mttip{font-weight:700;color:#202124;margin-bottom:6px}.mtmeta{font-size:14px}.mtkick{color:#ea4335;font-weight:600}.mtpred{color:#188038;font-weight:600;margin-left:10px}.mtres{margin-left:5px}.mtedit{display:block;font-size:12px;margin-top:6px;color:#1a73e8;cursor:pointer}.mtbox{background:#fff;padding:20px;border-radius:8px;width:320px}</style><div class="mtouter"><?php if(!is_user_logged_in()){?><div class="topbar"><button class="vipbtn" onclick="window.open('/football-livescore/','_blank')">⚽ Livescore</button><button class="vipbtn vipflash" onclick="vo()">👑 VIP Combo</button></div><?php }?><div class="mtwrap"><?php if(current_user_can('administrator')){?><div class="mtform"><input id="mttip" placeholder="Tip: Barcelona Vs PSG"><input id="mtpred" placeholder="Prediction: BTS @2.10"><input id="mtkick" placeholder="Time: 10:00"><button class="mtbtn" onclick="mp()">Post Tip</button></div><?php }?><div id="mtfeed" class="mtfeed"><?php if($r){foreach($r as $x){$d=date('d F Y',strtotime($x->created));?><div class="mtitem" id="tip<?php echo $x->id;?>"><div class="mtleft"><?php echo $d;if(current_user_can('administrator'))echo'<span class="mtedit" onclick="me('.$x->id.',\''.esc_js($x->matchtip).'\',\''.esc_js($x->pred).'\',\''.esc_js($x->kick).'\',\''.esc_js($x->result).'\')">Edit</span>';?></div><div class="mtright"><div class="mttip"><?php echo esc_html($x->matchtip);?></div><div class="mtmeta"><span class="mtkick">Kickoff: <?php echo esc_html($x->kick);?></span><span class="mtpred"> | Prediction: <?php echo esc_html($x->pred);?><span class="mtres"><?php echo esc_html($x->result);?></span></span></div></div></div><?php }}?></div></div></div><div id="mtmodal" class="mtmodal"><div class="mtbox"><button id="pb" class="mtbtn mtpurge" onclick="pu()">Purge Cache</button><button class="mtbtn mtdelete" onclick="md()">Delete Tip</button><div id="ps" class="mtstatus">Cache cleared ✓</div><input id="et" placeholder="Tip"><input id="ep" placeholder="Prediction"><input id="ek" placeholder="Kickoff"><input id="er" placeholder="✅ ❌"><button class="mtbtn" onclick="mu()">Update</button></div></div><div id="vipmodal" class="vipmodal"><div class="vipbox"><div class="vipclose" onclick="vc()">×</div><h3>VIP Combo costs $100 per week. This week you will receive carefully selected matches with strong value potential and realistic profit targets exceeding $1000.</h3><div class="vipslots">Only 20 VIP slots available this week.</div><a class="vipdonate" href="https://www.paypal.com/ncp/payment/X2FBFFPTQHL7A" target="_blank">🛒 Buy</a><form id="vf"><input type="hidden" name="subject" value="VIP Combo Inquiry"><input name="name" placeholder="Your name" required><input name="email" placeholder="Email" required><textarea id="vb" name="message" placeholder="Ask anything"></textarea><button type="submit">Send</button><div id="vm"></div></form></div></div><script>let a="<?php echo admin_url('admin-ajax.php');?>",b=document.body,c=0;function vo(){vipmodal.style.display="flex";b.style.overflow="hidden"}function vc(){vipmodal.style.display="none";b.style.overflow=""}window.onclick=e=>{if(e.target==vipmodal||e.target==mtmodal){vipmodal.style.display="none";mtmodal.style.display="none";b.style.overflow=""}};vb.oninput=function(){this.style.height="auto";this.style.height=this.scrollHeight+"px"};vf.onsubmit=function(e){e.preventDefault();fetch("/contact-form-handler.php",{method:"POST",body:new FormData(this)}).then(r=>r.json()).then(d=>{vm.innerText=d.message;if(d.success)this.reset()})};function mp(){let t=mttip.value,p=mtpred.value,k=mtkick.value;if(!t||!p||!k)return;if(!p.includes("@"))return alert("Prediction must contain @ odds");if(!k.includes(":"))return alert("Kickoff must contain :");let f=new FormData();f.append("action","mt_add");f.append("tip",t);f.append("pred",p);f.append("kick",k);fetch(a,{method:"POST",body:f}).then(r=>r.json()).then(d=>{if(d.error)return alert(d.error);mtfeed.insertAdjacentHTML("afterbegin",`<div class="mtitem" id="tip${d.id}"><div class="mtleft">${d.date}<span class="mtedit" onclick="me(${d.id},'${t}','${p}','${k}','')">Edit</span></div><div class="mtright"><div class="mttip">${t}</div><div class="mtmeta"><span class="mtkick">Kickoff: ${k}</span><span class="mtpred"> | Prediction: ${p}<span class='mtres'></span></span></div></div></div>`);mttip.value=mtpred.value=mtkick.value=""})}function me(i,t,p,k,r){c=i;et.value=t;ep.value=p;ek.value=k;er.value=r;mtmodal.style.display="flex";b.style.overflow="hidden"}function mu(){let f=new FormData();f.append("action","mt_update");f.append("id",c);f.append("tip",et.value);f.append("pred",ep.value);f.append("kick",ek.value);f.append("res",er.value);fetch(a,{method:"POST",body:f}).then(r=>r.json()).then(d=>{document.querySelector("#tip"+c+" .mttip").textContent=d.tip;document.querySelector("#tip"+c+" .mtkick").textContent="Kickoff: "+d.kick;document.querySelector("#tip"+c+" .mtpred").innerHTML=" | Prediction: "+d.pred+"<span class='mtres'>"+d.res+"</span>";mtmodal.style.display="none";b.style.overflow=""})}function md(){if(!confirm("Delete this tip?"))return;let f=new FormData();f.append("action","mt_delete");f.append("id",c);fetch(a,{method:"POST",body:f}).then(()=>{document.getElementById("tip"+c).remove();mtmodal.style.display="none";b.style.overflow=""})}function pu(){pb.innerText="Purging...";let f=new FormData();f.append("action","mt_purge");fetch(a,{method:"POST",body:f}).then(()=>{ps.style.display="block";pb.innerText="Purge Cache";setTimeout(()=>ps.style.display="none",2000)})}</script><?php return ob_get_clean();});
How This Actually Works
- Auto-creates its own database table. The
init hook checks for a matchtips table on every load and creates it if missing, so there’s no manual database setup required after adding the code.
- Auto-expires old tips. The same
init hook deletes any tip older than 7 days on every page load, keeping the table from growing indefinitely with stale predictions nobody needs anymore.
- Validates input before saving. The
mt_add handler rejects predictions without an @ (used to denote odds) and kickoff times without a :, catching obvious formatting mistakes before they reach the database.
- Reuses deleted IDs. Rather than always incrementing, the insert logic looks for the first gap in existing IDs and reuses it — a stylistic choice that keeps ID numbers compact, though it’s worth knowing this isn’t standard WordPress or MySQL practice.
- Restricts every action to administrators. Each AJAX handler checks
current_user_can('administrator') before doing anything, so only logged-in admins can post, edit, delete, or trigger a cache purge.
- Renders entirely through a shortcode. Dropping
[match_tips_feed] into any post or page outputs the full tips list, admin posting form (visible only to admins), and the VIP promotional modal in one self-contained block, styles and JavaScript included.
Practical Notes Before You Deploy This
- Set your correct timezone. The
date_default_timezone_set('Africa/Nairobi') line should match your actual audience’s timezone or your match-data source, not be left as a placeholder.
- The
mt_purge handler references a specific post ID (20719). That’s calling wp_update_post on a fixed post to trigger a cache-clearing plugin hook — you’ll need to swap that for whatever post ID your own cache-purge trigger depends on, or replace it with a direct call to your caching plugin’s purge function instead.
- The contact form posts to
/contact-form-handler.php. That endpoint isn’t included here, so you’ll need your own handler at that path (or point the fetch call at whatever contact-form processing script you’re actually using).
- Reconsider absolute profit claims in monetized copy. Marketing language that promises specific dollar returns from betting tips can run into problems with payment processor policies and, depending on your jurisdiction, gambling advertising regulations — a value proposition framed around pick quality rather than guaranteed profit tends to hold up better long-term.
Getting Match Data
None of the code above pulls in real fixtures or odds automatically — tips are entered manually through the admin form. If you want to eventually automate fixture data, sports data APIs like Sportmonks or API-Football are common choices, though building genuinely predictive models on top of that data requires real data science expertise rather than just displaying raw fixtures.
Join The Discussion
Are you running (or planning to run) a football predictions site on WordPress? Share whether you’re going the custom-code route or leaning on an existing plugin like Football Pool, and if you’ve experimented with monetization — ads, subscriptions, or a VIP tips tier — it’d be great to hear what’s actually converted for you versus what didn’t move the needle.