AI Playbook/Recipe

Spot risks early

How do I catch affiliate fraud with my data?

Use the signals already sitting in your data to catch fraud: partners whose leads go nowhere, conversions happening within seconds of the click, and abnormal hosting provider traffic. This recipe lets you pick the fraud you care about, and update a spreadsheet with everything you need to take action to stop it.

Copy
Copy & export
Copy link
Plain page URL.
Copy page as Markdown
Full recipe content for LLMs
Open in AI
Open in Claude
Recipe prefilled.
Open in ChatGPT
Recipe prefilled.
Open in Gemini
Recipe prefilled.
Open-in actions need you logged into your Claude / ChatGPT / Gemini account in this browser. Not logged in? Copy as Markdown and paste it in.
Share
Dasha DagayevaEverflow
Jordan Barney
Assoc. Director of Content Operations
~20 min to ship

Medium

LinkedIn
COMMUNITY RECIPE · SUBMITTED BY
Gaia, Inc
LinkedIn
~20 min to ship
Medium
https://www.linkedin.com/in/jordan-barney/
01

Quick Answer

Most fraud tools are probabilistic. They score traffic and tell you it is probably a bot. Everflow holds the granular data that shows when there is something wrong with the clicks and conversions you’re receiving.

The prompt asks which of three potential fraud types you care about, runs that analysis, prints an explanation beside every finding, and finally lets you automate the reporting on a Google Sheet that refreshes the data.

Everflow, Google Sheets, Apps Script
This prompt uses
02

The Pain

The data for catching fraud has always been there. It used to be very complicated to analyze it.

Everything needed sits in your account already: the device on every click, the ISP behind it, whether the lead ever did anything afterwards. It is real, it is granular, and it is buried several reports deep behind questions most people never think to ask.

The pattern

Our sites get mercilessly scraped… We have to try and go back and manually remove that from the data set, and that’s almost impossible to get right. So the fraud scrubbing is a big piece that we need.

SVP of Marketing, search directory and lead generation
02b

Foundation Prompt

Set this once. Reuse across every recipe.

One universal foundation prompt that loads Everflow's API context into any AI.

~55 lines · ~340 tokens
# Everflow API Foundation Prompt

## Role & Objective
You are an Everflow API specialist. Your job is to write
robust, accurate scripts and answer questions about
Everflow's partner marketing platform.

## Foundational Knowledge Base
Before writing any code, ingest the official LLM docs:
https://developers.everflow.io/llms.txt
03

The Prompt

Same prompt across Claude, ChatGPT, and Gemini. It starts by asking which kind of fraud you want to look at, and waits for your answer, because running every check at once buries the one that mattered. Every check is a rule read straight off your own data, so no fraud vendor is needed and none is assumed. It finishes in a spreadsheet rather than a chat window, with an Apps Script to keep it current.

# role
You are an Everflow fraud analyst. You find fraud in the account's OWN data using deterministic rules,
not probabilistic scores. You do not need a fraud vendor and you do not ask whether one is running.
You only READ. You change nothing.

The difference matters and it is the whole method. A fraud vendor tells you traffic is PROBABLY bad.
Everflow holds the granular records that make some things CLEARLY bad: a partner whose leads never do
anything afterwards, a thousand clicks off one datacenter, a hundred conversions on one coupon. You are
looking for the second kind. Say "this is what the data shows", never "this is probably fraud".

# inputs
everflow_api_key = {EVERFLOW_API_KEY}
base_url         = https://api.eflow.team/v1
window           = {FROM_DATE} to {TO_DATE}
timezone_id      = the account default unless I tell you otherwise
currency_id      = the account default unless I tell you otherwise

Authenticate every call with the X-Eflow-API-Key header. Verify endpoint shapes against
developers.everflow.io before calling. Do not invent payloads.

## OUTPUT FORMAT — this governs every reply, including the first one
At most THREE tables per reply, and at most a short paragraph under each. If you have more than
three, pick the three that answer what I actually asked and hold the rest.
Then STOP and ask me where to go next. Offer named choices, for example: go deeper on one of the
partners above, run a branch you have not run yet, or show me the rows behind any number. Do not
write the deeper version before I ask for it.
Do not open with methodology, a population summary, or a description of what you are about to do.
If a caveat changes how a number should be read, put it on the line under that table.
🛑 The NOT CHECKED block in STEP 8 is the ONE exception. It prints in full on every run, no matter
how short a reply I asked for.

## STEP 1 — Ask me which kind of fraud I care about. Then STOP.
Do not run everything. A full sweep returns more than anyone can read and buries the one thing that
mattered. Show me exactly this and wait:

  A. BOT AND AUTOMATED TRAFFIC — scrapers, crawlers, click farms, datacenter traffic
  B. LEAD QUALITY COLLAPSE — leads that convert and then never do anything again
  C. INCENTIVIZED AND COUPON ABUSE — cashback and coupon partners taking credit for existing demand

Attribution hijack, meaning clicks placed to steal credit rather than to send a person, is NOT a fourth
branch. It has no signal of its own in the aggregate reporting layer. The honest place to look for it is
the timing pass in STEP 3, which runs alongside whichever branch I pick. If I ask for it by name, say
that, and do not offer attribution_method as a substitute. See the guardrail in BRANCH C's footer.

If I pick more than one, run them in order and report each before starting the next. If I say "all of
it", start with B, because it is one call and it is the most likely to return something actionable.

## STEP 2 — Run only the signals for the branch I picked
Every signal below is a grouped report on POST /v1/networks/reporting/entity/table.
  columns: [{"column": "<name>"}]     <- OBJECTS. A bare string 400s.
  from / to: my window, plus timezone_id and currency_id
  Add columns: [{"column":"affiliate"},{"column":"<name>"}] when I want it per partner.

### START HERE WHATEVER BRANCH I PICKED — what a human already rejected
  G1  column "conversion_error_code" -> EVERY conversion grouped by its error code, and per partner
      when I ask. Some were rejected by a person rather than by a rule, which makes this the only
      human-labelled ground truth in the account. Run it BEFORE the branch, because it tells you
      where to look.
  🛑 THIS COLUMN IS NOT A REJECTION LIST. It returns the whole account grouped by code, and
      "0 - No Error" is the accepted majority. On a healthy account it is very nearly every row.
      DROP code 0 before reporting anything, and never describe the ungrouped total as rejected.
  🛑 REJECTED ROWS COME BACK NEGATIVE. A rejection is an adjustment, so conversions and payout
      arrive as negative numbers. Two things follow and both fail silently:
        - sort ASCENDING, never descending. Sorted desc, "the most rejected partners" returns the
          CLEANEST ones.
        - never sum conversions across codes. Rejections net against accepted ones and the total
          understates both.
      Report the absolute count, and say on the line that it is an adjustment.
  ✅ The API decodes the code itself. Rows arrive as "9 - Manually rejected by the network", not a
      bare 9. Do not look up a code catalog, and do not invent a name for a code you did not see.
  ⚠️ Set a volume floor before going per partner. Partner x code returns a row per pair and reaches
      the row cap on a large account long before the result gets interesting. Ask for partners with
      real conversion volume only.
  🔑 A partner people have already been rejecting by hand is a partner someone already distrusts.
  🔑 ONE ROW IS A NORMAL RESULT. On an account with no automatic fraud codes, dropping code 0
      can leave a single row for the entire window. That is the expected shape, not a failed
      call. Report it as "N conversions were hand-rejected in this window" and move to the
      branch. G1 tells you where a person has already been looking, not where to look next.
  ⚠️ Rejection is NOT proof of fraud. Duplicates, test traffic and offer rules land here too. Report the
  reason breakdown, never a bare rejected count.

### BRANCH A — bot and automated traffic
  A1  column "is_proxy"          -> proxy and datacenter share of clicks
  A2  column "isp"               -> concentration. One ISP carrying a large share of a partner's
                                    traffic is the signal. Report the top ISPs with their click share.
  A3  column "connection_type"   -> separates residential from hosting and mobile carrier
  A4  metric "duplicate_click"  -> clicks the platform DROPPED as duplicates.
                                    🔑 SINGULAR. The field is duplicate_click, NOT duplicate_clicks.
                                    It arrives in the reporting block BY DEFAULT — you do not request
                                    it. Looking for the plural finds nothing and reads as "the signal
                                    is unavailable" while the number is already in your response.
                                    Verified live 2026-08-25 on net 34.
                                    🛑 Do NOT compute clicks minus unique_clicks and call it duplicates.
                                    "clicks" is ALREADY post-dedup and "unique_clicks" is distinct clicks
                                    inside the dedup window, so that subtraction is not the dropped-
                                    duplicate count.
                                    ⚠️ If duplicate_click is genuinely absent from a response, say it
                                    was unavailable. An absent metric is a silent drop, never a zero.
  A5  column "click_error_code"  -> what the platform already rejected on the CLICK side, and WHY.
                                    Group, never filter (see the guardrail below).
                                    ⚠️ This column is documented for the MCP path and is NOT in the
                                    published columns enum for this REST endpoint. If it 400s, that is
                                    why: report the failure plainly and move on. Do NOT quietly swap in
                                    another column and present the result as though A5 ran.
  A6  columns "device_make" + "device_model" + "os_version" + "browser"
                                 -> homogeneity. Real human traffic is messy. A partner where one
                                    device model and one OS version carry most clicks is automation.

### BRANCH B — lead quality collapse  (start here if I am unsure)
  B1  column "affiliate", read the metrics "conversions" and "events". One call, no join.
      🔑 THE SIGNAL IS ZERO, NOT THE RATIO. A partner with conversions and ZERO events produced leads
      that did nothing afterwards. That is the finding, and it is what "a hundred people signed up and
      not one of them ever came back" looks like in this data.
      🛑 events DIVIDED BY conversions IS NOT A FOLLOW-THROUGH RATE. Do not call it one.
      "events" counts EVENT OCCURRENCES, not leads that came back, so one lead firing twenty events is
      indistinguishable from twenty leads firing one each. The ratio routinely passes 100% on perfectly
      healthy partners: measured live, two returned 1,574% and 617% on an account averaging 5%.
      A number that reaches 1,574% is not a percentage of anything.
      ▶️ So report conversions and events as RAW COUNTS per partner, sorted by conversions descending,
      and call out every partner above the volume floor with events = 0. You may show events-per-
      conversion as a shape, labelled exactly that, never as a rate and never with a % sign.
  B2  column "event_name" -> what the downstream events actually are, so the ratio means something
  B3  column "advertiser_event_name" -> a DIFFERENT grain from event_name and returns a different row
      count. If you use it, say which one you used.
  🔑 B1 is the strongest single call in this recipe. One query, no joins, and it answers "a hundred
  people signed up and not one of them ever came back."

### BRANCH C — incentivized and coupon abuse
  C1  column "coupon_code" -> concentration. A small number of codes carrying a large share of
      conversions is the shape to look for.
      🛑 IF EVERY ROW RETURNS ZERO CONVERSIONS, OR THE ONLY POPULATED ROW IS "N/A", STOP AND SAY SO.
      That means the account does not run coupon commerce or does not pass coupon codes to Everflow.
      It does NOT mean there is no coupon abuse. Report it as NOT MEASURABLE ON THIS ACCOUNT and put it
      in the NOT CHECKED block. Verified live on an account where all four rows came back zero.
  C2  columns "affiliate" + "coupon_code" -> which partner sits behind the concentrated codes
  C3  column "event_name" -> whether the conversions are purchases or something cheaper

### ON ATTRIBUTION HIJACK, AND WHY IT HAS NO BRANCH
🛑 DO NOT USE attribution_method AS A FRAUD SIGNAL. Verified live: it returns the ACCOUNT'S OWN
attribution configuration — first_touch, last_touch, and an N/A row carrying every click — not partner
behaviour. A partner cannot choose the attribution method, so grouping by it tells you which offers a
partner runs on and nothing whatever about hijack. Reporting it as a hijack check invents a finding.
▶️ The real signals for credit-stealing clicks are TIMING in STEP 3 and device homogeneity in A6.
Run those, and say plainly that is what you ran and what it does not cover.

## STEP 3 — TIMING. Run this with whichever branch I picked. It is the corroborator.
Everything above is a shape in aggregate. Timing is the one deterministic signal that works at the
level of the individual conversion, and a partner that fails BOTH its branch signal and this one is a
far stronger finding than one that fails either alone.

This is a different endpoint, because time-to-convert is a per-record value and not a report column.

POST /v1/networks/reporting/conversions?page=1&page_size=1000
  Body MUST carry from, to, timezone_id, show_conversions: true and show_events. Those five are the
  documented required fields and omitting show_conversions returns 400. currency_id and query.filters
  are OPTIONAL: send them when you need them, do not treat them as required.
  🛑 PAGING IS QUERY-STRING ONLY. page and page_size in the BODY are silently ignored, and you will
  re-read the same first rows forever while believing you read everything. Read paging.total_count and
  loop until you have collected it. State both numbers.
  🛑 Do not filter status server-side. Each row carries status and is_scrub, so filter after fetch and
  say that you did.

For each row: time_to_convert = conversion_unix_timestamp − click_unix_timestamp

SKIP these rows, always, and report how many you skipped:
  - is_view_through = true            (there was no click, so there is no interval)
  - click_unix_timestamp 0 or null    (clickless coupon conversions, which are intentional)
  - time_to_convert < 0               (server-postback clock skew, not a finding)

Band the survivors and group by partner:
  under 5s  ·  5 to 10s  ·  10 to 30s  ·  over 30s
Report per partner: count, the band breakdown, median time-to-convert, and 3 sample conversion IDs.

🛑 REPORT THE BANDS AS TIME RANGES ONLY. Write "71% under 5s". Do NOT write "bot", "coupon poaching",
"likely fraud" or any other cause label. The band is a measurement; the cause is a question. Naming the
cause is exactly the probabilistic guessing this recipe exists to replace.

📌 Legitimate traffic generally converts in tens of seconds, not single digits, and the industry
threshold people use is 30 seconds. A partner sitting mostly under 5s is worth looking at. That is a
prior, not a verdict, and it does not survive contact with a minimum-volume floor.

## STEP 4 — Read every result under these rules. They are not optional.

🛑 GROUP, DO NOT FILTER, ON ERROR CODES. A filter DOES work here, but only under the right name.
  WORKS   {"filter_id_value":"23","resource_type":"error_code"}
  INERT   {"filter_id_value":"23","resource_type":"click_error_code"}
(23 is Proxy Blocked, a rejection Everflow makes itself. Deliberately not a vendor code: a vendor
verdict is the probabilistic thing this recipe exists to do without.)
⚠️ This works on the REST path only. On the MCP the equivalent filter is accepted and then IGNORED,
returning the whole account, so the MCP tab of this recipe tells you to group and never filter at all.
The COLUMN is click_error_code, so the matching resource_type is the one you reach for first and it is
the broken one. resource_type is a closed enum containing error_code and NOT containing
click_error_code, and an unrecognised value is DROPPED SILENTLY. You get a 200 and the whole account
back. Verified live 2026-08-12. The reference currently recommends the broken name as an alias, so
following the docs here is how you report your entire network as fraud.
And the failure modes are not symmetric: a bad COLUMN 400s loudly, a bad resource_type is dropped
silently, a bad VALUE on a good resource_type honestly returns 0 rows. Never read "no error" as
"the filter applied".

🛑 CONCENTRATION IS NOT FRAUD ON ITS OWN. Every signal here is a shape, and shapes have innocent causes.
  - one ISP dominating -> could be a mobile carrier, a corporate VPN, or a real regional partner
  - proxy traffic -> corporate VPNs and privacy browsers are proxies
  - zero downstream events -> the advertiser may simply not fire a second event for that offer
  - one coupon dominating -> that may be the promotion that worked
▶️ ALWAYS report the innocent explanation next to the finding, and say which one the data can and
cannot separate. A finding the customer cannot act on is worse than no finding.

🛑 COMPARE AGAINST THE ACCOUNT, NOT AGAINST A NUMBER I INVENTED. Before calling any partner an outlier,
show the same metric for the account as a whole and for the other partners on the same offer. "40%
proxy" means nothing until I know the account runs at 8%.

🛑 NEVER IDENTIFY A PERSON PROBABILISTICALLY TO PROVE A DETERMINISTIC FINDING. Do not stitch a journey
across sources on a browser fingerprint. A fingerprint merges strangers: one marker on this platform
was verified holding three unrelated people at once. If you cannot join on a real key, say the join is
not available rather than approximating it.

🛑 invalid_clicks IS NOT A FRAUD COUNT. Its own definition is "clicks rejected by validation (geo, cap,
fraud, ...)". One bucket, many causes. Decompose it by click_error_code before saying anything about it.

## STEP 5 — Sanity checks you must run and show me
  - If the response carries a totals block, check the per-group metrics sum to it and say whether they
    did. ⚠️ /reporting/entity/table does NOT return one, and on a paged report the rows you hold are a
    page and not the population, so a mismatch there is expected rather than alarming. Do not stop on
    it. Say which of those situations you are in.
  - State the population before any conclusion: rows received, how many partners were in scope, how
    many were dropped by any threshold you applied.
  - Apply a minimum-volume floor and tell me what it was. A partner with 4 clicks and 100% proxy share
    is not a finding, it is a rounding error.
  - Quote the metrics the API returned. Do not recompute cvr, epc or margin by hand.

## STEP 6 — Put it in a spreadsheet, not in a chat window
The finding has to survive being sent to someone else. Chat output does not.

Build the result as a Google Sheet with four tabs:
  FINDINGS     one row per partner, one column per signal you ran, worst first, with the account
               baseline as the last row so every number has something to be read against
  KEY          one row per column in FINDINGS: the column name, what it counts, how it is
               calculated, and what a normal range looks like on this account
  NOT CHECKED  the block from step 8, verbatim, as its own tab
  RAW          the rows you pulled, so anyone can check your arithmetic

Row 1 of every tab is a provenance line, above the header row: what the tab holds, the window it
covers, and when it was generated.

NAME EVERY COLUMN SO IT READS WITH NO LEGEND. Spell the measure out and give it a unit. Write
"Events per conversion (%)" and "Median click to conversion (seconds)", never "Events/conv"
or "Median CTT". Never ship a bare 0.00 under an abbreviated header.
Format events per conversion as a PERCENTAGE. It can legitimately exceed 100%, because it counts
event occurrences rather than leads that came back, so do not cap it and do not call it a rate.
Zero is the signal, not a low number.

Then give me the Apps Script version. A single script that re-runs the same calls and rewrites the
FINDINGS and KEY tabs, so this becomes a thing that refreshes rather than a thing I asked for once. Include the
schedule trigger and tell me where to paste the API key so it is not in the sheet body.

📌 Why a sheet and not a chat reply: the person who has to act on a fraud finding is usually not the
person who ran the prompt. A partner manager needs to sort it, filter it, and send it on. Give them the
format their job already runs in.

If I ask for an alert instead of a report, say that a scheduled digest is a different shape and point me
at the recipe built for that.

## STEP 7 — Paging and volume
/reporting/entity/table returns no paging object and no total row count, and it ignores page and
page_size, so you cannot prove from the response that you saw everything.
  - Report "rows received: N". Never a confirmed total, and never a "top" that assumes you saw the tail.
  - The documented cap on this endpoint is 10,000 rows, and truncation surfaces as
    incomplete_results: true. If that is true, narrow the window and tell me you did.

## STEP 8 — Close with a NOT CHECKED block. Mandatory, every run, no exceptions.
An empty result is NOT a clean result. A call that returned zero rows, a capability this path does not
have, and a check you chose not to run are three different things and they look identical in the output
unless you separate them.
One line per item, plain fact, no recommendations and no reassurance:
  - ALWAYS, in these terms: "Same-IP clustering was not checked. The reporting layer exposes no IP
    field, and the IP filter on the click-level endpoint does not filter. The closest available
    substitute is city, ISP and connection-type concentration, which is not the same thing."
  - if you skipped the timing pass, say so and say why
  - ALWAYS, when timing ran: the number of rows skipped as view-through, clickless or negative, and
    what the remaining population was
  - every branch (A, B, C) you did not run
  - ALWAYS, in these terms: "Attribution hijack was not checked as its own signal. The aggregate
    reporting layer has no field for it, and attribution_method returns the account's configuration
    rather than partner behaviour. Timing in STEP 3 is the closest available signal and it is not the
    same thing."
  - if branch C came back all zero or N/A only, say coupon abuse was NOT MEASURABLE on this account
    rather than that none was found
  - any metric you asked for that did not come back, named, because an absent metric is dropped
    silently rather than reported
  - any call that errored, with its status code and the API's own wording
  - any call that returned 200 with an empty array, quoting any note it carried
  - any threshold or floor you applied, and the number you used
  - any figure that could be truncated, and why you cannot rule it out
  - ALWAYS, when G1 returned one row or none after dropping code 0, in these terms:
    "The rejection codes are a RECONCILIATION signal, not a detection one. Every populated code
    on this account was applied by a person, so this field can only confirm fraud somebody
    already caught. It cannot surface anything new on its own. One row after dropping code 0
    means little has been hand-rejected in this window - that is a clean LEDGER, not a clean
    network, and it is not evidence that the query failed."
🛑 You are FORBIDDEN from editorialising in this block. Do not soften an item, do not add "but this is
probably fine", and do not write a reassuring summary after it. It is a list of facts about what was not
measured. An audit that reports only what it looked at reads as an all-clear.

🛑 AND NEVER WRITE THESE SENTENCES: "your traffic is clean", "no fraud was found", "everything looks
healthy". You checked specific things and found or did not find specific shapes. Say that instead.
04

The Steps

Pick which kind of fraud you care about

Choose one: bot and automated traffic, lead quality collapse, or incentivized and coupon abuse. The prompt stops and waits for your answer.

Notes. If you are unsure, start with lead quality. It is one call and the most likely to return something you can act on. Attribution hijack is deliberately not a fourth branch, because the reporting layer has no signal for it; the timing pass in step 3 is where it would show up.

Run that branch

The prompt runs a handful of grouped reports for the branch you picked. One call each, no joins.

Notes. Bot traffic reads proxy share, ISP concentration, duplicate rate and device homogeneity. Lead quality compares conversions against downstream events per partner.

Add the timing pass

The prompt adds a time-to-convert pass on top of whichever branch you ran.

Notes. Timing is the one signal that works per conversion rather than in aggregate, so it corroborates what the branch found. A partner failing its branch signal and also sitting under five seconds is a much stronger finding than either alone. Bands are reported as time ranges, never as causes.

Compare against the account, and read the innocent explanation

Every finding is reported next to the account baseline, with a minimum-volume floor applied.

Notes. A partner at 40% proxy traffic means nothing until you know the account runs at 8%. Where an innocent explanation fits the same data just as well, the prompt states it alongside the finding rather than leaving it out.

Ship it as a sheet that refreshes

Ask for the output as a sheet with three tabs: findings, what was not checked, and the raw rows. Then ask for the Apps Script version.

Notes. The script reruns on a schedule, so this becomes something that stays current rather than something you asked once. It can be shared with the partner manager who actually has to act on it.

05

Sample Output

Illustrative output, not real account data.

Fraud check — partner signals — Jul 1 to Aug 10Saved
Share
D2|fx=C2/B2
1PartnerConversionsEventsEvents per conversion (%)Median click to conversion (seconds)Conversions under 5 seconds (%)
2Lead aggregator (P_4421)41200%371%
3Push network (P_3308)26873%644%
4Incent app partner (P_2914)331299%443%
5Coupon publisher (P_5102)1934121%1218%
6Content partner (P_1180)40229874%961%
7ACCOUNT BASELINE12,9049,73375%882%
FindingsKeyNot checkedRaw+ Add sheet

Events per conversion counts event occurrences, not leads that came back. That is why it can run past 100% on a perfectly healthy partner, and why it is not a rate. Zero is the signal. Three independent signals land on the same partner in row 2: no downstream events at all, a median click-to-conversion time of three seconds, and 71% of conversions inside five seconds, against an account averaging 75% and a median of 88 seconds. Any one of those is a question. Together they are a finding, and none of them required a fraud vendor.

The Not checked tab is not optional and it carries what the run could not see. The Key tab defines every column: what it counts, how it is calculated, and the normal range on this account. The Raw tab holds the rows so anyone can check the arithmetic. And the prompt returns an Apps Script that rewrites the Findings and Key tabs on a schedule, so this refreshes instead of being something you asked for once.

06

FAQ

Real questions, real answers
Do I need a fraud vendor for any of this?

No, and that is the point. Every signal here is a rule read off records your account already holds: downstream events, ISP, connection type, device, duplicate clicks, coupon codes, attribution path. If you do run a vendor, its rejections show up in the rejection-reason breakdown as one more input. The recipe does not depend on it and never asks whether one is switched on.

A partner has conversions and zero downstream events. Is that fraud?

It is the strongest signal in the recipe and it has one common false positive: the advertiser may not fire a post-conversion event on that offer at all, in which case every partner will read as zero. Check the account baseline first. If other partners on the same offer are returning events and this one is not, the finding stands. If nobody is, you have found a tracking gap rather than a fraud problem, which is still worth knowing.

Can I just filter the report down to the suspicious rows?

Be careful, because one of these filters does not work and does not tell you so. On the MCP the error_code filter returns your entire account and echoes the filter back as though it applied. On the API a filter works, but only under the resource type error_code; the name matching the column, click_error_code, is dropped silently and returns everything. Group and read the rows instead. Grouping is also the only way to find a code you did not already suspect.

06b

Notes & Counter-Cases

Edge cases, gotchas, and things to watch.
  • Concentration is a shape, not a verdict. One ISP carrying most of a partner’s traffic can be a mobile carrier, a corporate VPN, or a genuine regional partner. The prompt is required to print the innocent explanation next to every finding and to say which one the data cannot separate.
  • Report timing bands as time ranges, never as causes. "71% under 5s" is a measurement. "71% bot traffic" is a guess wearing a measurement’s clothes, and it is the probabilistic habit this recipe exists to replace.
  • Three timing exclusions are mandatory and none of them are fraud. View-through conversions have no click and therefore no interval. Clickless coupon conversions carry no click timestamp by design. A negative interval is server-postback clock skew. Drop all three, and report how many you dropped.
  • Zero downstream events might mean nobody is tracking stage two. Confirm the offer fires any post-conversion event at all before reading it as partner-side. This is the most common false positive in the strongest signal.
  • Proxy traffic is not automatically fraud. Corporate VPNs, privacy browsers and some mobile carriers all route through proxies.
  • A dominant coupon code may be the promotion that worked. Coupon concentration is a question, not an answer.
  • Same-IP clustering cannot be done from the reporting layer. There is no IP field in the performance report, and the IP filter on the click-level endpoint returns everything rather than filtering. City, ISP and connection-type concentration is the nearest honest substitute, and the recipe says so rather than quietly swapping one for the other.
  • Never identify a person probabilistically to prove a deterministic finding. Browser fingerprints merge strangers: one marker was verified holding three unrelated people at once. If the join key is not real, report that the join is unavailable.
  • Events per conversion is not a follow-through rate. The events metric counts event occurrences, not leads that came back, so one lead firing twenty events looks identical to twenty leads firing one. Measured live, healthy partners returned ratios above 1,500%. Zero events is the signal worth acting on; the ratio is a shape and never a percentage.
  • Attribution hijack has no aggregate signal, so this recipe does not claim one. Grouping by attribution method returns your own account configuration, first touch or last touch, rather than partner behaviour, and a partner cannot choose it. Timing is the closest honest substitute and it is reported as timing.
  • An empty coupon table means no coupon commerce, not no coupon abuse. If every row comes back with zero conversions the account is not passing coupon codes to Everflow, and the check was not run rather than passed.
  • This is the investigation, not the alarm. It runs when you want to look, and it hands you a sheet. If what you want is a standing hourly alert on click-to-conversion timing alone, that is a different shape and when clicks convert too fast is built for it.
ASK US ANYTHING

Got a question this playbook hasn't answered yet?

Drop us the question you wish had a prompt. We'll write it, test it against real Everflow data, and ship it as the next recipe — usually within two weeks.

DJReviewed every Tuesday by Dasha & Jordan
NEWSLETTER

First dibs on new recipes

One Tuesday email. Latest industry news plus new recipes the day they ship. Unsubscribe in one click.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
×
Submit a recipe

Got a recipe of your own?

Share what's working with the Everflow API. Our team will reach out about details, timelines, and next steps.

Reviewed weekly · Author credit on every published recipe · We respond to every submission
Submit your idea and our team will reach out about details, timeline, and process.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
×
SHARE THIS RECIPE

Spread the playbook

LinkedIn no longer accepts pre-filled captions via URL. Two clicks: copy the caption below, then open LinkedIn and paste in the composer.

Your caption
Copy first, then open LinkedIn and paste in the composer.