I see that I receive the price concessions by email since I signed-up for the CPE newsletter - I think it will be best to use these emails as the data source than scraping
Assessed 2026-09-26 — this page is a research record, not a design. The built plan is
Price Concessions — Phase 5 Ingestion.
Kept: Matthew's steer above (the origin of the route), and the mailbox folder scoping from the
Email alerts section — adopted into the plan's §3 as the poll's target, with the caveat that the rule must be
server-side (a webmail-client-only filter is not reliable).
Rejected: the third-party inbound-parse webhook (SendGrid/Mailgun/Postmark) and a new public ingest
endpoint — a processor and an inbound surface for a sovereign-hosted DSPT Category 3 platform, for data that
already lands in a mailbox we control; the regex-over-text parser with float money (the store's rule is
integer pence) and a mock whose figures do not match the live data; and letting Hermes be the poller.Correction, later the same day (2026-09-26). Two real alerts were read from the live mailbox and the parse was
validated against them — phase 5 spec §16.
One thing above was wrong: the mock's(previously £85.45)form is real — the alert emails carry amendments
in exactly that shape, and £92.86 is the amended price for that line. My assessment said otherwise and it was
mistaken. What still stands against the snippet is the float money, and its assumption that the message is
whitespace-separated text with those columns: the shipped parser reads thetext/plainpart's fixed-width
columns, and the HTML is a mailing-platform (Mailchimp) wrapper with eleven tables.On the Email alerts section's premise — checked, and it is not true of this installation (2026-09-26):
"your agent is Hermes and it is already configured to automatically scan your inbox every 15 minutes". In fact
hermes cron listreports no scheduled jobs, the email platform variables (EMAIL_IMAP_HOST,
EMAIL_ALLOWED_USERS, …) are commented out in the agent's environment file, and the mail CLI those skills
assume is not installed. It is buildable, but it is not a short-cut. And even built, the agent must not
be the ingestion path: the store's value is that a published price is traceable to a retained message and
reproducible — a release, an attempt row, a hash, and an arithmetic gate anyone can re-run. An LLM in that
chain makes the step non-deterministic and unverifiable, would require the agent to write the platform's
production tables directly, and would only run while an agent session is up — the one thing a 4-core host shared
with OpenCode must never assume. The mailbox credentials belong in the app host's environment, and no model
goes near this path. Where an agent read is genuinely useful: as an independent cross-check — a second
route to the same message, the technique that verified phase 4 by reading NHSBSA's page live — and as a
human-facing alert when the poll is down. That is verification, not ingestion.
Using the CPE email updates as your primary data source is an exceptionally smart, resilient engineering choice for VeriPath Pharmacy.
Websites change their HTML structure, classes, and layouts constantly, which makes web scrapers brittle. Email notifications, on the other hand, are sent out in structured, predictable templates. By processing these incoming emails, you turn a pull-based scraping model into a highly reliable, event-driven ingestion pipeline.
Here is how you can technically architect this email ingestion engine within your SIAAS / Central Tier framework, the risks involved, and how to handle validation.
Because VeriPath uses a sovereign hosting model where the client's data is isolated, you should handle the email collection at the Central Tier (VPS1) level and distribute the data cleanly downward via your existing application tunnels.
Instead of manually checking an inbox, you can automate this using an Inbound Email Webhook Service (such as SendGrid Inbound Parse, Mailgun, or Postmark), or a dedicated self-hosted IMAP worker.
[ CPE Dispatch System ] ──(Email Sent)──► [ Custom Mailbox: concessions@veripath.co.uk ]
│
▼
[ Inbound Email Webhook ]
│
(HTTP POST JSON Payload)
│
▼
┌─────────────────────────────┐
│ Central Tier (VPS1) API │
│ "Email Parser Endpoint" │
└──────────────┬──────────────┘
│
▼
[ HTML/Text Regex Extraction ]
│
▼
[ Central Reference DB ]
concessions@veripath.co.uk) and sign it up for the CPE alerts.api.veripath.co.uk/v1/email-ingest).When an email arrives from CPE via a webhook, your application receives the raw text or HTML content. The script below uses Python to extract data from a typical text representation of the CPE table layout:
import re
import json
def parse_cpe_email_payload(email_body, email_subject):
"""
Parses the incoming text body of a CPE price concession email alert.
Expects a standard layout matching the September 2026 data structures.
"""
# 1. Identify the dispensing month from the subject line or body text
# e.g., "September 2026 Price Concessions 6th Update"
month_match = re.search(r'(January|February|March|April|May|June|July|August|September|October|November|December)\s+20\d{2}', email_subject)
target_month = month_match.group(0) if month_match else "Current Month"
# 2. Split body into lines for parsing tab/whitespace-separated rows
lines = email_body.split('\n')
extracted_concessions = []
# Regex to capture: Drug Name | Pack Size (digits) | Price string starting with £
# e.g., "Chlordiazepoxide 5mg capsules \t 100 \t £92.86 (previously £85.45)"
row_pattern = re.compile(r'^(.+?)\s+(\d+)\s+(£\s*\d+\.\d{2}.*)$')
for line in lines:
line_str = line.strip()
# Skip header metadata rows
if not line_str or "Drug" in line_str or "Pack size" in line_str:
continue
match = row_pattern.match(line_str)
if match:
drug_name = match.group(1).strip()
pack_size = int(match.group(2).strip())
raw_price = match.group(3).strip()
# Clean pricing string to find the dominant active price
clean_price_match = re.search(r'£\s*([0-9]+\.[0-9]{2})', raw_price)
price = float(clean_price_match.group(1)) if clean_price_match else None
# Detect if it's a re-determination/amended record
is_amended = "previously" in raw_price.lower()
extracted_concessions.append({
"month_year": target_month,
"drug": drug_name,
"pack_size": pack_size,
"price_concession": price,
"is_amended": is_amended,
"raw_price_string": raw_price
})
return {
"source": "CPE Email Notification",
"month": target_month,
"count": len(extracted_concessions),
"data": extracted_concessions
}
# Example Mock Inbound Data Execution
mock_subject = "September 2026 Price Concessions 6th Update"
mock_body = """
Drug Pack size Price concession
Acamprosate 333mg gastro-resistant tablets 168 £43.28
Chlordiazepoxide 5mg capsules 100 £92.86 (previously £85.45)
"""
parsed_output = parse_cpe_email_payload(mock_body, mock_subject)
print(json.dumps(parsed_output, indent=2))
While much more reliable than web scraping, you must put three explicit checks in place to guarantee structural compliance before the data drops down into your pharmacy inventory caches:
cpe.org.uk). If you don't validate DKIM/SPF signatures, anyone could spoof an email to your endpoint and inject malicious pricing rows to skew a pharmacy's purchasing logic.To map this ingestion workflow tightly to your development roadmap, let me know:
Since ==your agent is Hermes and it is already configured to automatically scan your inbox every 15 minutes==, you do not need Roundcube to push external alerts or connect to an API. [1]
Instead, the cleanest approach is to use Roundcube to isolate and highlight the sender's emails, allowing Hermes to find them immediately during its 15-minute sweep.
Depending on how you want Hermes to process these messages, here are the two best options to configure inside Roundcube:
If you only want Hermes to look at emails from specific high-priority senders (and ignore everything else to save processing cycles or avoid noise), you can route those specific senders into a distinct folder.
Hermes-Alerts).From -> matches string -> type the sender’s email address.Hermes-Alerts folder..env or configuration file, ensure you change the targeted IMAP folder path from INBOX to Hermes-Alerts. [2, 3]If Hermes is scanning your main INBOX but you want it to programmatically filter for these specific emails, you can have Roundcube automatically "star" or "flag" the emails the moment they arrive.
From matches the target sender's email.FLAGGED, allowing it to instantly identify the alert emails without reading your entire inbox. [2, 3]As an added layer of security, keep in mind that Hermes has its own built-in sender restrictions. If you are using the Hermes Agent Email Bot framework, you can strictly control who it responds to by updating the Hermes environment configuration directly: [4]
.env file and look for the EMAIL_ALLOWED_USERS variable.EMAIL_ALLOWED_USERS=boss@company.com,client@domain.com). [4, 5]By combining a Roundcube folder filter (to keep the agent's workspace clean) with the Hermes allowlist, your agent will seamlessly catch and process those specific sender alerts every 15 minutes without any manual intervention. [5]
[1] https://www.roundcubeforum.net
[4] https://hermes-agent.nousresearch.com