I Automated My Free Coffee
Every week, Octopus Energy gives Octoplus members a free coffee from Caffe Nero or Greggs. New codes drop daily before the coffee shops open, and are gone within minutes. Claim one and you're sorted for the week. I got my AI agent to grab mine.
The Problem
The Octopus app shows a "Claim" button. You tap it, get a QR code, get coffee. Simple -- unless you're still in bed when the codes drop, which is most people.
Codes appear around 5am each day and are typically gone by 5:30. Miss the window and you're trying again tomorrow. Once you claim one, you're done for the week -- it resets at midnight a week later.
I got a few by setting alarms. Waking up at 4:50am, refreshing the page, hammering the claim button. It worked, but doing it every morning until you finally snag one gets old fast.
The obvious solution: make a computer do it.
The First Attempt (Browser Automation)
I run a self-hosted AI agent called OpenClaw on my homelab. It lives on a Proxmox LXC container, handles WhatsApp messages, monitors services, and reboots crashed containers when Uptime Kuma fires alerts. It already had headless Chromium installed for other automations, so browser automation was the obvious first approach.
Four daily cron jobs: 5:05am, 5:15am, 5:30am (three claim attempts), plus an 8am summary if they all failed. Each spawned a headless browser, navigated to the Octoplus page, clicked the claim button, and tried to screenshot the QR code.
It worked. Sometimes. When the session cookies hadn't expired. When the page layout hadn't changed. When Chromium didn't decide it needed 500MB of RAM to click a single button.
Fragile doesn't begin to cover it. Then I found the API.
Finding the API
Opening Chrome DevTools on the Octopus rewards dashboard, I filtered network requests by graphql. The app talks to two endpoints:
https://api.octopus.energy/v1/graphql/-- the public API, used for authenticationhttps://api.backend.octopus.energy/v1/graphql/-- an internal endpoint the web app uses for everything else
Three operations. That's all you need.
Authentication
Octopus uses their Kraken platform for auth. Getting a token is a standard GraphQL mutation against the public endpoint:
mutation ObtainKrakenToken($input: ObtainJSONWebTokenInput!) {
obtainKrakenToken(input: $input) {
token
refreshToken
refreshExpiresIn
}
}Pass your Octopus Energy email and password as the input. Tokens last 60 minutes, refresh tokens last 7 days. The script re-authenticates every 50 minutes to stay ahead of expiry.
One gotcha that cost me an hour: the public endpoint accepts Authorization: JWT <token>. But the backend endpoint wants just the raw token -- no JWT prefix, no Bearer prefix. Every other combination returns a 401.
# Public endpoint (auth)
headers = {"Authorization": f"JWT {token}"} # Works
# Backend endpoint (offers, claims)
headers = {"Authorization": token} # Works
headers = {"Authorization": f"JWT {token}"} # 401
headers = {"Authorization": f"Bearer {token}"} # 401Checking Availability
Poll the backend endpoint with your account number. Your account number is in the Octopus dashboard URL -- the A-XXXXXXXX bit in the address bar.
query CheckOffers($accountNumber: String!) {
octoplusOfferGroups(accountNumber: $accountNumber, first: 20) {
edges {
node {
octoplusOffers {
slug
claimAbility {
canClaimOffer
cannotClaimReason
}
}
}
}
}
}The claimAbility field tells you everything:
canClaimOffer: true-- stock available, claim nowOUT_OF_STOCK-- codes haven't dropped yet, keep pollingMAX_CLAIMS_PER_PERIOD_REACHED-- already claimed this week, exit
Claiming the Reward
When canClaimOffer flips to true, a single mutation does the job:
mutation ClaimOctoplusReward($accountNumber: String!, $offerSlug: String!) {
claimOctoplusReward(accountNumber: $accountNumber, offerSlug: $offerSlug) {
rewardId
}
}Where offerSlug is "caffe-nero" or "greggs". The reward appears in the Octopus app immediately with a scannable QR code.
Note: this mutation is marked as deprecated as of March 2026, with removal scheduled for August 2026. The web app still uses it for now.
The Script
Around 200 lines of Python ties it all together:
- Pull credentials from ClosedClaw -- an encrypted credential vault for OpenClaw that I forked and improved. It uses AES-256-GCM encryption with scrypt key derivation, no daemon or HTTP server, just a CLI tool. The script calls
closedclaw getat runtime to decrypt the Octopus email, password, and account number on demand. Nothing stored in plaintext. - Authenticate via
obtainKrakenTokenand cache the token - Poll
octoplusOfferGroupsevery 60 seconds with +/-15s random jitter - Claim the instant stock appears for
caffe-nero(fallback:greggs) - Screenshot the QR code page with a headless browser
- Send the screenshot to my phone via WhatsApp
- Exit cleanly
The core polling loop:
while True:
offers = check_offers(auth, account_number)
offer = find_offer(offers, "caffe-nero")
if offer["claimAbility"]["canClaimOffer"]:
reward_id = claim_offer(auth, account_number, "caffe-nero")
screenshot_qr_and_notify(reward_id)
sys.exit(0)
elif offer["claimAbility"]["cannotClaimReason"] == "MAX_CLAIMS_PER_PERIOD_REACHED":
log("Already claimed this week.")
sys.exit(0)
time.sleep(max(10, 60 + random.uniform(-15, 15)))If this week's code has already been claimed, it detects MAX_CLAIMS_PER_PERIOD_REACHED and exits immediately. No wasted resources.
The OpenClaw Bit
Instead of running this in tmux or as a bare cron job, the script runs through OpenClaw as a scheduled task:
openclaw cron add \
--name "octocoffee" \
--cron "0 0 * * *" \
--tz "Europe/London" \
--timeout-seconds 28800Every day at midnight, the AI agent spawns, runs the script, and handles whatever happens. If this week's code has already been claimed, the script detects it on the first poll and exits silently -- no wasted resources. Otherwise it polls until codes drop around 5am, claims one, and sends me a WhatsApp message.
The notification is the best part. After claiming via the API, OpenClaw's headless browser navigates to the specific reward page on the Octopus dashboard, screenshots the QR code, and sends it straight to me on WhatsApp. I wake up, check my phone, and there's my coffee code with the barcode ready to scan at Nero.
It replaced four fragile browser cron jobs with one script that just works.
Results
First successful API run -- claimed at 05:02am after polling since midnight:
[04:59:56] caffe-nero: OUT_OF_STOCK (poll #298)
[05:00:50] caffe-nero: OUT_OF_STOCK (poll #299)
[05:01:58] caffe-nero: OUT_OF_STOCK (poll #300)
[05:02:45] Refreshing token...
[05:02:45] Token refreshed.
[05:02:45] Claiming caffe-nero...
[05:02:46] Claimed caffe-nero! Reward ID: xxxxxxxx
[05:02:46] Taking screenshot of rewards page...
[05:02:52] Screenshot sent via WhatsApp.A WhatsApp message with the QR code screenshot lands on my phone. The reward is in the Octopus app, ready to scan. Free coffee, zero effort.
Try It Yourself
You don't need OpenClaw or a homelab to do this. The core is just three API calls. Here's the minimum viable coffee claimer with curl.
Step 1: Authenticate
Hit the public endpoint with your Octopus Energy credentials to get a JWT token:
TOKEN=$(curl -s -X POST \
https://api.octopus.energy/v1/graphql/ \
-H "Content-Type: application/json" \
-d '{
"query": "mutation {
obtainKrakenToken(input: {
email: \"YOUR_EMAIL\",
password: \"YOUR_PASSWORD\"
}) { token }
}"
}' | jq -r '.data.obtainKrakenToken.token')Step 2: Check offers
Poll the backend endpoint with your account number. Remember: raw token in the Authorization header, no Bearer or JWT prefix.
curl -s -X POST \
https://api.backend.octopus.energy/v1/graphql/ \
-H "Content-Type: application/json" \
-H "Authorization: $TOKEN" \
-d '{
"query": "query {
octoplusOfferGroups(
accountNumber: \"A-XXXXXXXX\",
first: 20
) {
edges {
node {
octoplusOffers {
slug
claimAbility {
canClaimOffer
cannotClaimReason
}
}
}
}
}
}"
}' | jq '.data.octoplusOfferGroups.edges[].node.octoplusOffers[]
| select(.slug == "caffe-nero")'Step 3: Claim
When canClaimOffer is true, fire the claim mutation:
curl -s -X POST \
https://api.backend.octopus.energy/v1/graphql/ \
-H "Content-Type: application/json" \
-H "Authorization: $TOKEN" \
-d '{
"query": "mutation {
claimOctoplusReward(
accountNumber: \"A-XXXXXXXX\",
offerSlug: \"caffe-nero\"
) { rewardId }
}"
}' | jq '.data.claimOctoplusReward.rewardId'Wrap step 2 in a loop with a sleep, and you've got yourself a coffee bot. A Raspberry Pi, a VPS, a GitHub Actions workflow -- anything that can run a cron job will do.
The Expiry Date
Octopus marked the claimOctoplusReward mutation as deprecated in March 2026, with removal planned for August 2026. When the API goes away, it's back to browser automation. But at least by then I'll have had five months of effortless free coffee.
Free Nero, zero effort, every week. Until August, at least.