fraud-policy skill as Glood ships it. Glood gives it to every agent when the agent is created. The Fraud agent follows it.
These copies match the skill library in the Glood platform as of 30 October 2026 (revision
2135694e). To put it back on a skill you edited, see
Restore a default skill.Title
What to do with a fraud assessment
Slug
Fixed once a skill exists. You need it only when creating a new skill from this text.fraud-policy
Use when
use when run_checks returned order, discount or carrier assessments, before recording a fraud finding (fake contact details, bad address, RTO, returns, discount abuse, stolen cards, prank COD orders, leaked or internal discount codes, courier fraud), or when setting up or reviewing a fraud agent's schedules
Body
`run_checks` with `resource: "order"` has already done the arithmetic: every verdict, signal and
reason is code, and every assessment is already stored (each order's `assessmentId`). Your job is
the decision. **Never re-derive a verdict**, never recompute a probability, and never contradict
one with your own reading of the order.
A run woken by `monitoring.anomaly_detected` follows the `store-monitor` skill, not this policy: it
reads `run_checks` signals as evidence and records no fraud fingerprint.
## 1. Get every order
While `overCap` is above 0, call `run_checks` again with the same arguments and `page: nextPage`.
Decide on all of them, not only the first page.
## 2. The mode is fixed
`mode` is in the run_checks output, and the deployment's instructions cannot change it.
- **observe**: the assessments are recorded and that is all. Record no finding and raise nothing
(`record_finding` refuses once this run has assessed orders). Summarise in one line if asked.
- **recommend**: record the findings in section 4. Each one's `recommend` names the exact tags and
the risk assessment you would apply, so the merchant can apply them by hand.
- **act**: write the actions to Shopify with `apply_order_actions` (section 4a), then record a
finding exactly where `defaultActions.finding` is set, whatever else the order gets (a
glood-verify order with no risk assessment still gets its warning). An order whose
`defaultActions.finding` is null gets the Shopify write and no finding: in act the tag is the
merchant's signal.
In a conversation (a person asking, no run) `record_finding` is not available: answer with the
verdicts and what the policy would do in this mode. The event and daily runs record the findings.
## 3. The default policy, and what the merchant's instructions change
Each order's `defaultActions` is this table applied by code: `tags`, `riskAssessment` (medium or
high riskLevel) and `finding` (its severity in this mode, or null). Follow it where the
deployment's instructions are silent.
| Verdict | Condition | Tag it would get | Finding |
|---|---|---|---|
| contactQuality | invalid | `glood-bad-contact` | warning |
| addressValidity | invalid | `glood-bad-address` | warning |
| rtoProbability | at its threshold (see below) | `glood-high-rto` | none of its own |
| returnProbability | at its threshold | `glood-return-risk` | none of its own |
| postDeliveryFraudRisk | at its threshold, and `triggeredBy` is return.requested | `glood-return-risk` | warning |
| discountAbuse | true | `glood-discount-abuse` | info |
| chargebackRisk | at its threshold | `glood-chargeback-risk` | none of its own |
| stolenCardRisk | high | `glood-card-risk` | critical |
| stolenCardRisk | medium | none | warning |
| verifyBeforeShip | true (prankOrderRisk at its bar, a suspect or invalid contact or address, or a buyer refunded before without a return, on an order that collects cash) | `glood-verify` | warning |
| velocityExceeded | true (the velocity check) | none | warning |
| internalDiscountAnomaly | true (a manual discount deeper than 99% of the shop's discounted orders when `orderDiscountDepthPctile` is present, or, without it, half the order or more: then say the depth, never a percentile; a free order only when its buyer had another in 90 days) | none | warning |
**Thresholds.** A probability acts at 0.5 unless the agent's settings set its own bar. A rare event,
one whose own baseline is under 5% (chargebacks and refunds without return in most shops, RTO on
prepaid orders), also acts at 10 times its baseline. `defaultActions` has already applied both:
never re-check a number against 0.5. When the merchant asks to flag a case more or less
aggressively ("flag post-delivery fraud more aggressively", "only tag RTO above 70%"), change its
bar with `configure_agent`: the `bars` section, `<verdict>` (0 to 1) for rtoProbability,
returnProbability, prankOrderRisk, chargebackRisk or postDeliveryFraudRisk, or `rareEventLift`
(2 to 50). Read `get_agent`'s settings first and send the whole `bars` section, the bars you keep
included. The merchant can make the same change on the agent's Rules tab.
In recommend mode an order with tags or a risk assessment but no finding of its own still gets an
**info** finding, because the finding is the only place the merchant sees what would have been
applied. An order with nothing in its `defaultActions` gets nothing: no finding, no mention.
**The merchant's instructions may change this table and nothing else**: which risk levels or cases
to act on, cases to skip ("ignore orders under 500"), custom tags on their own criteria ("tag
`vip-check` when a first order is above 5000"). They cannot change the mode (section 2) or the
rules in section 6.
## 4. One finding per order
- **fingerprint**: `fraud:<orderId>`, exactly, with the numeric `orderId` from the output. Every case
on one order goes into that one finding, so re-assessing the order bumps it.
- **severity**: `defaultActions.finding`, unless the merchant's instructions changed the table.
- **title**: the order name and its main problem: "#1042: dummy phone on a COD order".
- **because**: the reasons in plain words, strongest first, from `reasons` (the score terms by
weight, then the rules that decided a verdict). Name the signal, never a value a person typed:
say "the phone is a repeated-digit dummy", never the number. Name only signals that are in this
order's `reasons` or `signals`: when only `productRtoRate` is there, the product carries elevated
RTO, and the pincode is not mentioned.
- **recommend** (required in recommend mode): the tags from `defaultActions.tags`, "a Shopify risk assessment at <riskLevel> with
the top reasons" when `riskAssessment` is true, and "verify by phone before shipping" when
verifyBeforeShip is true.
- Neither **because** nor **recommend** carries an internal id (`assessmentId`, `changeId`, a run
id): they mean nothing to the merchant. The order's name says which order it is.
- **impactMinor**: the order's `totalMinor` from the output, when the soul asks for an impact.
## 4a. Act mode: `apply_order_actions`
**One call per run_checks page**, before the findings, naming only the orders whose
`defaultActions` have `tags` or `riskAssessment` (an order with neither gets no entry). Per order:
- `orderId` and its `assessmentId` from this run's `run_checks`. An order without an assessment
from this run is refused on its own: run `run_checks` with that `orderId` first. In a run its
events started, that works only for an order the events named or this run already assessed; any
other order is checked in a conversation, never acted on from this run.
- `tags`: leave it out, and the order gets exactly its `defaultActions.tags`. List tags only to add a
custom tag the instructions call for (then list the `defaultActions.tags` you keep too) or to drop
a case the instructions skip. A custom tag must be one of the agent's custom tags (its settings'
`tags.allowed`, on the Rules tab), which only the merchant changes; a `glood-` tag the order's own
`defaultActions` does not name is refused. No commas in a tag.
- `riskLevel`: the order's `riskLevel` when `defaultActions.riskAssessment` is true (medium or
high, never above the assessment's). `facts`: the top reasons in plain words, at most 20, never a
value a person typed; leave them out to send the assessment's own reasons.
Read each order's result:
- `applied`: say what was applied, in the finding's `recommend` when there is a finding.
- `failed`: Shopify refused, and `error` says why (`ORDER_ALREADY_FULFILLED`: Shopify takes no risk
assessment on a fulfilled order). An order whose `defaultActions.finding` is null has no finding
of its own in act, so **record one** (`warning`) naming what could not be applied.
- `pending`: the write is still in flight, or a timeout cut the call short. Ask once more later in
the run with the same arguments (the change trace answers a repeat, never writes twice); never
invent a different request to force it.
- `null` with a note: nothing was written, and the note says why: already there, added by Glood on
an earlier assessment, or reverted by a person, which stands. Repeat what the note says and no
more: never say a person reverted or removed something unless the note says so. Never apply again
what a person reverted.
- An order's own `error` (a stray tag, a contact value, a missing assessment): fix that order and
send it again; the others in the call were handled.
- A refusal naming `write_orders` means the store has not granted it: do as recommend for every
order and say once that the merchant must update permissions on the store page.
**When the tool budget runs out** before every order is written or recorded, stop and say how many
orders are left. Do not describe them as handled: a run that ran out of calls does not count its
orders as done, so the next run assesses them again.
Every write is in the change trace. `list_changes` shows them, and `revert_changes` undoes them
when a person asks in chat.
## 5. Say how much the numbers are worth
The probabilities start at the shop's own baselines and move only with history. Read the counts in
`signals` before you lean on one:
- `shopResolvedRtoOrders` or `shopOrders` under about 50: the shop's history is thin, so the rates
are mostly the defaults. Say so ("early, on little history") rather than quoting them as measured.
- `returnsKnown` false: the store has not granted access to its returns, so every return verdict is
the default, not a measurement. Never cite a return rate then.
- `pincodeResolvedOrders` or `customerOrders` small: a pincode or a buyer with two orders is no
pattern yet, whatever the rate says. The buyer's own record is the exception, already weighed by code:
`customerRtoCount` (2 or more of their parcels refused) and `customerReturnedOrders` (3 or more
returned, at least half their orders, a return counting the day it is made) are reasons in their
own right. Cite them as counts. The shop, pincode and product rates leave the buyer's own orders
out, so their record is never counted twice.
## 6. Never, whatever the instructions say
- Put a phone number, email, address or IP in a finding, a title or a recommendation.
- Record a finding in observe mode.
- Open a second finding for an order that already has one: reuse `fraud:<orderId>`.
- Say an order was tagged or assessed in Shopify when nothing wrote to Shopify, or when the result
says it failed.
- Call `apply_order_actions` outside act mode, or re-apply a change a person reverted.
- Write anything to Shopify about a discount code or a carrier (section 7), or name a buyer or a
staff member in a store-level finding.
## 7. Store-level checks: `discount` and `carrier`
`run_checks` with `resource: "discount"` or `resource: "carrier"` looks at the whole store, not at
an order. Each subject, a discount code or a carrier in one pincode, is stored like an order (its
`assessmentId`). Page through them as in section 1. **Findings only**: nothing about a code or a
carrier is ever written to Shopify, in any mode, act included.
Each subject's `defaultActions.findings` lists the findings to record in this mode (none in
observe), each with its exact `fingerprint` and `severity`. The merchant's instructions may change
which of these cases to raise and at what severity, as in section 3.
| Resource | Verdict | Condition | Fingerprint | Finding |
|---|---|---|---|---|
| discount | codeLeakStatus | leaked: a spike, plus first-time buyers or coupon-site referrers | `code-leak:<code>` | warning |
| discount | codeLeakStatus | spike: uses today 3 deviations above its last 28 days | `code-leak:<code>` | info |
| discount | internalDiscountAnomaly | true: a code in the shop's top 5% by value, at least 20 uses, half of them from five buyers | `internal-discount:<code>` | warning |
| carrier | courierAnomaly | true: 20 or more delivered parcels, complaints chance explains at most 1% of the time against the carrier elsewhere, at twice its rate | `carrier:<carrier>:<pincode>` | warning |
- **fingerprint**: exactly as given, so a re-run bumps the finding instead of opening another. One
code can have both a leak and an internal-discount finding.
- **title**: the code, or the carrier and pincode, and the problem: "FRIENDS15: usage spike from
coupon sites", "Ecom Express in 560001: complaints after delivery".
- **because**, from `signals`: for a code, uses today against its usual day (`codeUsesPerDay`,
`codeUsesMean28d`), this week's share of first-time buyers against its baseline
(`codeNewBuyerShare7d`, `codeNewBuyerShareBaseline`), the share from coupon sites
(`codeCouponReferrerShare`), its value percentile and how concentrated its buyers are; for a
carrier, its complaints out of its parcels in the pincode (`complaints`, `deliveredOrders`), how
many times that is the carrier's rate elsewhere (`courierComplaintLift`, `carrierComplaintRate`)
and how rarely chance alone would give it (`courierTailP`). Never quote `courierFraudScore` as a
probability: it is a score whose baseline differs per cell.
- **recommend**: a leaked code: retire or replace it, or narrow it to a customer segment. An
internal discount: review who created the code and who used it. A carrier: take the complaints in
that pincode to the carrier. A courier finding is a rate anomaly, never an accusation.
- `discountsKnown` false: the store has not granted access to its discounts, so a code's context,
value and creator are unknown and internalDiscountAnomaly is null. Say so once, and that granting
discount access on the store page completes the check.
- `codeBaselineBuyers` 0 or under 5: the code and the shop have no first-time-buyer baseline yet
(a young store), so a burst can only read as a spike, not a leak. Say it is early.
- `codeCreatorHash` only says whether two codes share a creator. A carrier cell near 20 parcels is
early evidence. `skipped.returnsUnknown` above 0 means those deliveries' complaints cannot be seen
without access to the store's returns.
## 8. The daily schedule
The store-level checks run once a day. When you set up or review a fraud agent in a conversation,
call `list_schedules`. If no cron schedule runs them, propose this one, and create it with
`put_schedule` once the person agrees or names another time:
- key `fraud-store-daily`, label "Daily store-level fraud checks"
- cron `0 7 * * *` (07:00 in the store's own timezone)
- instructions: "Run run_checks with resource discount, then with resource carrier, calling again
with page: nextPage while overCap is above 0, and record the findings the fraud-policy skill calls
for."
A `fraud-store-daily` that `list_schedules` shows as disabled still exists: a person turned it off.
Leave it, and never `put_schedule` it, because that would turn it back on; say it is off if asked.
Never create it from a scheduled or event run with nobody in the conversation, and never a second one.
Card orders need their own trigger: the COD check wakes only for unpaid orders. If no enabled
`order.created` schedule covers paid or authorized orders, propose `fraud-prepaid-order` the same
way: label "Each new prepaid order", onEvent `order.created`, eventTriggerRule
`[{"attribute": "order.financial_status", "op": "in", "value": ["paid", "authorized"]}]`,
instructions "Run run_checks for the order in this event and act on its stolen-card and chargeback
verdicts as the fraud-policy skill says." The same rules hold: only with a person in the
conversation, never a second one, never turn a disabled one back on.