Skip to content
1Claw Academy
Curriculum/Transactions & Treasury2 minAdvanced · Lesson 12 of 16

Lab: get charged three times for one order

Lab

Retry a request the way every HTTP client does, then add the one header that makes the retry safe.

The payment cards lesson said an Idempotency-Key is required on order, and required is easy to read as bureaucratic. This shows what the header is actually preventing, which is the most ordinary failure in distributed systems: a response that never arrived.

  1. 1

    Model a charge endpoint with and without idempotency, then retry the way a client does after a timeout.

    bash
    cat > /tmp/idem.py <<'EOF'
    charges, seen = [], {}
    
    def charge(amount, key=None):
        if key is not None:
            if key in seen:
                return f"replayed, no new charge (total {sum(charges)})"
            seen[key] = True
        charges.append(amount)
        return f"charged {amount} (total {sum(charges)})"
    
    print("no key: the response timed out twice, so the client retried twice")
    for _ in range(3):
        print("  ", charge(25))
    
    charges.clear(); seen.clear()
    print("\nwith a key: same three attempts")
    for _ in range(3):
        print("  ", charge(25, key="order-abc-123"))
    EOF
    python3 /tmp/idem.py
  2. 2

    Three attempts, one order, and a seventy-five dollar difference.

    text
    no key: the response timed out twice, so the client retried twice
       charged 25 (total 25)
       charged 25 (total 50)
       charged 25 (total 75)
    
    with a key: same three attempts
       charged 25 (total 25)
       replayed, no new charge (total 25)
       replayed, no new charge (total 25)
  3. 3

    Clean up.

    bash
    rm /tmp/idem.py

The client was not buggy. A timeout tells you the response did not arrive and tells you nothing about whether the server acted, so retrying is the correct behaviour and the only safe way to do it is to make the second attempt recognisable as the same request.

  • The key must be generated by the caller and reused across retries of one logical operation. Generating a fresh key per attempt is the same as having none.
  • 1Claw scopes replay protection to a 24-hour window and compares a hash of the body, so the same key with a different body is a conflict rather than a silent replay.
  • This is why an agent doing anything irreversible needs a stable key: an agent that retries on error is an agent that will eventually retry a purchase.
Watch out

The dangerous version of this bug is invisible in testing, because it only appears when a response is lost. It shows up first in production, during an incident, when everything is already timing out.

Tip

The same reasoning covers bootstrap and spend-policy writes, which take the same header for the same reason. Anywhere a retry could duplicate an effect, the key is what makes the retry safe.

Check your understanding

3 questions
1

Why did the client retry at all?

2

What happens if the client generates a fresh key per attempt?

3

Why is this especially important for agents?