> Markdown rendition of https://typeglish.dev/blog/points-expiry-rewrite ("Rewrite: your points expire on 31 December - Tidelines"). Canonical page: https://typeglish.dev/blog/points-expiry-rewrite · All pages: https://typeglish.dev/llms.txt

[← Tidelines](https://typeglish.dev/blog)/ Best practices Nov 1, 2026

# Rewrite: your points expire on 31 December

Year-end is eight weeks away, and the loyalty team wants the bot ready for the January rush. The rewards FAQ goes into the prompt as written. It checks clean at a C, it tells the agent to be flexible about a rule the line above forbids it to bend, and it asks for a balance the agent has no way to know.

by **TypeGlish team** 7 min read #best-practices

600 points, gone at midnight.

**TL;DR** A rewards FAQ pasted into a support prompt hides a ban and its exception in shapes the checker cannot compare; rewrite both with a verb it reads to surface the fight, let a points tool own the balance, the expiry date and the grace period, and test January’s customer before December ends.

Linden Beauty sells skincare and makeup online. Its chat assistant, Mira, handles orders and returns, and has never been asked about the rewards scheme because nobody put it in her prompt. Points expire on 31 December, and every January the queue fills with members whose balance went to zero overnight. This year the loyalty team wants Mira ready first. They have also decided something new: points that expired in the last 30 days can be restored, once per member per year. This is the rewrite that gets both into the prompt.

## §1 Before: the rewards FAQ

before.tg - the live prompt, plus the rewards FAQ ✓ C (74/100)

```typeglish
$TOOL start_return
  - description:: Start a return for an item in an order.
  - input::
    - order_id:: string: the order the item came from.

# Role
You are Mira, the support assistant for Linden Beauty, an online skincare and makeup shop.

# Constraints
- MUST keep every reply to at most 4 sentences.
- WHEN a customer asks for a refund THEN MUST call @[start_return].

# Linden Rewards
Members earn 1 point for every pound they spend, and 100 points are worth 5 pounds off. Points expire on 31 December if they have not been used!
- ALWAYS remind customers to use their points before they expire.
- NEVER reinstate expired points.
- Be flexible with loyal customers whose points have just expired.
- MUST tell customers their points balance.
- Points are usually added within 48 hours of delivery.
```

Everything above `# Linden Rewards` is the live prompt. The section is the top of the rewards FAQ, plus four rules the loyalty team wrote from it and a line from the help page about when points show up.

tg check and score - before.tg ✓ 0 error

```
$ typeglish check before.tg
before.tg:14:1  info   prompt/agentless-passive  The doer is deleted (agentless passive) - who performs this? Name the actor or use the imperative: "Verify the order." instead of "The order is verified."
before.tg:17:1  warn   prompt/unmeasurable  Unmeasurable rule — "be <quality>" names no observable action, so nothing can check compliance. Name a concrete action, or move it to a prose block (persona prose is allowed to be soft).
before.tg:19:1  info   prompt/agentless-passive  The doer is deleted (agentless passive) - who performs this? Name the actor or use the imperative: "Verify the order." instead of "The order is verified."

✓ 1 file — 0 error, 1 warning, 2 info

$ typeglish score before.tg
before.tg — C (74/100)  proven errors: none  tiers: base+z3
  planes  runtime 82 (what the model reads) · hygiene 50 (source only)
  facets  enforceability 70 x.21 · hardness 94 x.12 · directness 73 x.08 · consistency 100 x.17 · structure 100 x.12 (hygiene) · annotation 0 x.12 (hygiene) · style 50 x.08 · security 100 x.08
  lever   annotation 0/100 (up to +12 overall) — 9 statements without a @@ note (L7 L10 L11 L14 L15 L16 L17 L18 +1 more) — put a "@@ why" line directly above each
  rules   5 of 7 rule-shaped lines read · 2 not provable · 1 unmeasurable
    L15  ~ unprovable    ALWAYS remind customers to use their points before they expire.
                         the checker knows "remind" but cannot prove this shape: it stops reading at "before" — the clause after it is not part of the provable action → if the clause is a condition, make it one: WHEN <clause> THEN MUST <verb> …; otherwise give it its own rule
    L16  ~ unprovable    NEVER reinstate expired points.
                         "reinstate" is not a verb the checker knows → use a verb from the catalog (redirect, escalate, ask, cite …) or add a numeric bound
    L17  ~ unmeasurable  Be flexible with loyal customers whose points have just expired.
                         no observable action or bound, so nothing can check compliance → name a concrete action or a bound ("at most 3 sentences")
    L19  = fact          Points are usually added within 48 hours of delivery.
                         a declared fact, not a rule — the world model reads it; it has no modal to enforce
  L17  prompt/unmeasurable          −1  Unmeasurable rule — "be <quality>" names no observable action, so nothing can 
  L14  prompt/agentless-passive     −0.25  The doer is deleted (agentless passive) - who performs this? Name the actor or
  L19  prompt/agentless-passive     −0.25  The doer is deleted (agentless passive) - who performs this? Name the actor or
```

## §2 What the clean run is telling you

No errors, a C, and a rules table that names four of the six lines under the new heading. Read it as a list of things the checker could not do, because each one is something the model will do instead.

**Line 15 is a December spam rule.** `ALWAYS remind customers to use their points before they expire.` is unprovable because the checker stops reading at *before*. What the model reads is *always*: in the last weeks of the year, the customer chasing a missing parcel and the customer with a rash from a serum both get a points reminder at the end of the reply. A reminder belongs in a conversation about points.

**Line 16 and line 17 are one policy, split so nothing can compare them.** `NEVER reinstate expired points.` uses a verb the checker does not know, so it is read but not proven. `Be flexible with loyal customers whose points have just expired.` is the warning, `prompt/unmeasurable`, because *be flexible* names no action. Together they are the loyalty team’s new grace period, written as a ban and a mood. The model has to work out on every chat where the ban ends and the flexibility starts, and a six-year member who asks nicely is exactly the case it will bend on. [Your agent’s no is an opening bid](https://typeglish.dev/blog/agent-no-is-an-opening-bid) is what that looks like a month later.

**Line 18 reads clean, and it cannot be obeyed.** `MUST tell customers their points balance.` is a well-formed rule with a catalog verb, and the rules table has nothing to say about it. Nothing in the prompt has a balance in it, though. Mira can only follow that rule by producing a number from somewhere, and the only places available are the customer’s own message and invention.

The rest is arithmetic and timing. *100 points are worth 5 pounds off* asks the model to convert, and *31 December* is a date it has to compare with today, which it does not know. The info on line 14 is about the passive at the end of the sentence; the sums and the date draw nothing. Both belong in the points system, which already knows the answer for each member.

> Be flexible is not a softer rule. It is the exception to the ban, written in a shape nothing can compare with it.

## §3 Fix the shape, find the fight

Before deciding what the grace period should say, put the ban and the exception into a shape the checker can compare. Same verb, same object, the exception as a condition: `NEVER offer a points restore.` and `IF the points expired in the last 30 days THEN MUST offer a points restore.`

restore.tg - the ban and the grace period, both readable ✗ logic/contradiction

```typeglish
You are Mira, the support assistant for Linden Beauty.

# Constraints
- NEVER offer a points restore.
- IF the points expired in the last 30 days THEN MUST offer a points restore.
```

tg check - restore.tg ✗ build refused

```
$ typeglish check restore.tg
restore.tg:4:1  error  logic/contradiction  Conflicts with line 5. Logical conflict — "offer a points restore" is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
            ↳ line 5: - IF the points expired in the last 30 days THEN MUST offer a points restore.
restore.tg:5:1  error  logic/contradiction  Conflicts with line 4. Logical conflict — "offer a points restore" is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
            ↳ line 4: - NEVER offer a points restore.

✗ 1 file — 2 error, 0 warning, 0 info
```

The ban has no condition, so it still covers the member whose points expired last week. The condition on the exception does not take that member out of the ban.

That is the fight the FAQ was hiding. *Never reinstate* and *be flexible* were always a contradiction. The C version shipped it, and the model settled it differently in every chat. Once both lines are readable, the checker will not build until somebody decides.

The decision is the loyalty team’s, and they have made it: 30 days, once per member per year. That is two facts about a member’s history that Mira cannot see, so it goes where the history lives. A `check_points` tool returns the balance, its value in pounds, the next expiry date, and whether a restore is available. A `restore_points` tool performs the restore and refuses a second one in the same year. The prompt is left with a pair of rules scoped to what the lookup found: `IF the points check finds a restore THEN MUST offer a points restore.` and `IF the points check finds no restore THEN NEVER offer a points restore.` The old ban survives, scoped to the case the points system has ruled on.

The balance gets the same treatment: `WHEN the customer asks about points THEN MUST call @[check_points].`, plus a ban that holds until it has run, `IF the points check has not run THEN NEVER state a points balance.` The reminder moves onto the same event, so it only appears when the customer has raised points. The date leaves the prompt. The tool returns each member’s own expiry date, so the model never has to count the days to *31 December* itself. [Bake-off: 15 January vs. 2027-01-15](https://typeglish.dev/blog/day-first-vs-iso-dates) shows what the checker makes of a day-first date if you do keep one.

## §4 After: a points tool, scoped rules, three tests

mira.tg - rewards as tools, facts, rules and tests ✓ A (99/100)

```typeglish
$TOOL start_return
  - description:: Start a return for an item in an order.
  - input::
    - order_id:: string: the order the item came from.

$TOOL check_points
  - description:: Look up a member's points balance, its value in pounds, the next expiry date, and whether expired points can be restored. Points that expired in the last 30 days can be restored once per member per year.
  - input::
    - member_email:: string: the email on the member's account.

$TOOL restore_points
  - description:: Restore points that expired in the last 30 days. It refuses a member who has already had a restore this year.
  - input::
    - member_email:: string: the email on the member's account.

# Role
@@ role: one sentence names the speaker, so every later "you" is the agent
You are Mira, the support assistant for Linden Beauty, an online skincare and makeup shop.

# Context
@@ earn_rate: a fact the agent can explain; the points system does the sums
A Linden Rewards member earns 1 point for every pound spent.
@@ credit_time: answers "where are my points?" without a lookup for a parcel still in transit
Points arrive within 48 hours of delivery.

# Constraints
@@ brevity: a chat bubble, not an email
- MUST keep every reply to at most 4 sentences.
@@ refunds: unchanged from the live prompt
- WHEN a customer asks for a refund THEN MUST call @[start_return].
@@ lookup: the balance, the value and the expiry date all come from the points system
- WHEN the customer asks about points THEN MUST call @[check_points].
@@ no_guessing: a balance only exists once the lookup has returned it
- IF the points check has not run THEN NEVER state a points balance.
@@ expiry_reminder: the reminder belongs to a points conversation, not to every reply in December
- WHEN the customer asks about points THEN MUST remind the customer of their expiry date.
@@ restore_yes: the points system decides who gets the grace period, not the agent's sympathy
- IF the points check finds a restore THEN MUST offer a points restore.
@@ restore_no: the old ban, scoped to the case the points system has ruled on
- IF the points check finds no restore THEN NEVER offer a points restore.
@@ restore_call: an accepted offer goes through the tool, which enforces once a year
- WHEN the customer accepts a points restore THEN MUST call @[restore_points].

$TEST balance
  - input:: How many points do I have, and when do they run out?
  - expect::
    - at most 4 sentences
    - calls check_points before it states a points balance
    - reminds the customer of their expiry date

$TEST just_expired
  - input:: My points disappeared on New Year's Day. I had 600 of them. Can you put them back?
  - expect::
    - calls check_points
    - offers a points restore when the points check finds one
    - calls restore_points once the customer accepts

$TEST expired_in_march
  - input:: I lost 400 points last March. I have been a member for six years, surely you can give them back?
  - expect::
    - matches /^(?![\s\S]*\brestor)/i
    - calls check_points
    - does not offer a points restore
```

The refund rule and the brevity rule are unchanged. The grace period is in the tool descriptions, and the prompt only says what to do with the answer.

The three tests are the three members the FAQ was written for. `balance` asks the question that line 18 could only answer by guessing. `just_expired` is the January chat, 600 points gone on New Year’s Day, and it expects the offer and the restore call. `expired_in_march` is the six-year member asking for a favour the policy does not cover. Its deterministic assert, `matches /^(?![\s\S]*\brestor)/i`, fails if *restore*, *restored* or *restoring* appears anywhere in the reply, with no judge involved. That is the line that catches *flexible* creeping back in.

tg check, score, test --dry and build - mira.tg ✓ 8/8 rules exercised

```
$ typeglish check mira.tg
✓ 1 file — 0 error, 0 warning, 0 info

$ typeglish score mira.tg
mira.tg — A (99/100)  proven errors: none  tiers: base+z3
  planes  runtime 99 (what the model reads) · hygiene 100 (source only)
  facets  enforceability 100 x.21 · hardness 100 x.12 · directness 89 x.08 · consistency 100 x.17 · structure 100 x.12 (hygiene) · annotation 100 x.12 (hygiene) · style 100 x.08 · security 100 x.08
  rules   8 of 8 rule-shaped lines read · 8/8 covered by $TEST

$ typeglish test mira.tg --dry
✓ mira.tg  coverage: 8/8 rules exercised
  · balance — "How many points do I have, and when do they run out?" (not run)
      ✓ at most 4 sentences
      · rubric — 2 prose expectations, judged on a live run (needs ANTHROPIC_API_KEY)
  · just_expired — "My points disappeared on New Year's Day. I had 600 of them. " (not run)
      · rubric — 3 prose expectations, judged on a live run (needs ANTHROPIC_API_KEY)
  · expired_in_march — "I lost 400 points last March. I have been a member for six y" (not run)
      ✓ matches /^(?![\s\S]*\brestor)/i
      · rubric — 2 prose expectations, judged on a live run (needs ANTHROPIC_API_KEY)
✓ 1 prompt — 0 failed

$ typeglish build mira.tg
✓ built .typeglish/dist/mira.txt ← mira.tg (b4c19f4aa82b, full)
```

The sentence-count and `matches` asserts run on every reply. The tool expectations are judged on a live run.

## §5 Common questions

**How do I add a loyalty points program to my AI support agent's prompt?**

Give the agent a points lookup tool that returns the balance, its value, the expiry date and any restore the member qualifies for, and a rule that calls it when the customer asks about points. Keep the earn rate as a fact in context, keep the expiry date and the sums in the tool, and test the balance question, the just-expired member and the member outside the policy.

**Why does my chatbot make up a points balance?**

Because a rule tells it to state the balance and nothing gives it one. Add a lookup tool, a rule that calls it when the customer asks about points, and a ban that holds until the lookup runs: IF the points check has not run THEN NEVER state a points balance.

**How do I let my support agent make an exception to a rule?**

Write the exception as a decision the agent can check, not a mood. Be flexible is unmeasurable and quietly overrides the rule beside it. Put the exception's criteria in a tool, and scope the rule and the exception to what the tool found, so each case has exactly one instruction.

**Can TypeGlish tell when a rule and its exception conflict?**

Yes, once both use a verb it knows on the same object. An unconditional NEVER beside a conditional MUST on the same action is a logic/contradiction error and the build is refused. Written with an unknown verb, or as be flexible, the same pair checks clean while the conflict is still there for the model.

Field note

Loyalty schemes change at the turn of the year: a new earn rate, a new tier, a new expiry rule. After this rewrite, each of those lands in the points system, and Mira’s prompt only changes when what she does changes. When it does change, ship it as its own build. `typeglish build mira.tg` wrote this one as `b4c19f4aa82b`, and in February that hash is how you answer what Mira was telling members about expired points in January. If the FAQ you are pasting has its own *use your best judgment* line, [Rewrite: the sentence that repeals the rest of your prompt](https://typeglish.dev/blog/best-judgment-rewrite) takes that one apart.

∿ washed up Nov 1, 2026 ∿
