DEV Community

Akshat Gupta
Akshat Gupta

Posted on

Teaching an AI assistant to forget a deleted Stripe customer

Here is a small billing horror story.

A customer asks to be deleted. Someone clicks delete in Stripe. A week later your AI assistant is still happily telling people what that customer pays and which invoice is open. It sounds completely sure. It is completely wrong.

Nothing crashed. The assistant just kept a memory of something that no longer exists.

I wanted to fix that for Stripe. So for Mergetober I built a connector for Cognee. The code lives in PR #394 for issue #4547.

Billing data is a good test for a connector. Nothing in it stands alone. A charge belongs to an invoice, an invoice to a subscription, a subscription to a customer. If a connector loses those links, the assistant loses the story.

What it does

Cognee turns documents into a knowledge graph that an agent can search. The connector feeds it from Stripe. It reads customers, subscriptions, invoices, charges, products and disputes. Then:

  • Every object becomes a short document.
  • Related objects stay connected. A customer, its subscription, the invoice, the charge and the dispute can all be reached from each other.
  • Later runs read only what changed.
  • Anything you delete in Stripe is forgotten.
await cognee.remember(
    stripe_source(),  # key and account come from the environment
    dataset_name="stripe",
    primary_key="id",
    write_disposition="merge",
)
Enter fullscreen mode Exit fullscreen mode

It never reads card numbers or payment methods. Each document is built from a short list of allowed fields, not from the raw object. A field Stripe adds next year cannot sneak in by accident. Email, phone and metadata are off unless you ask for them.

Two choices I changed my mind about

Links. My plan was to treat Stripe rows as a relational table and let Cognee build the links from foreign keys. Then I checked. The staging tables dlt writes to SQLite have no foreign keys at all, even when you declare them. So the links would never appear.

The fix was simpler. Every document lists the things it belongs to: customer:cus_..., subscription:sub_..., product:prod_.... Cognee stores those as shared nodes. Two documents that share a node are connected. No guessing, and no model involved.

Changes. Stripe's list calls can only filter by creation date. That sounds fine until someone pays an old invoice. The invoice was created last month, so a date filter never sees it again.

So later runs read Stripe's events instead. One request asks what happened since last time. Then only the objects named in those events are fetched again. If nothing happened it costs one request. If the events are older than Stripe keeps them, the connector lists everything again and compares.

The real test

First I tested against a fake Stripe. 62 tests, no network. But a fake only proves what you thought to fake.

So I made a free Stripe test account, seeded it with three customers, and gave the connector a read-only key that can look but not touch.

The first sync took in 25 objects across all six types and Cognee built the graph with OpenAI. I asked which customers have unpaid invoices. The answer was Charliecorp, with an open invoice for $120.00. I asked which customers have a disputed charge and what plan they are on. It followed the trail from the dispute to the charge to the customer to the subscription to the plan and answered fraudulent, needs response, Pro plan at $49 a month.

Then I synced again without changing anything. One request. Nothing written.

Then I renamed a customer and deleted another straight from the Stripe dashboard. The next sync made three requests, wrote one object and forgot one. The deleted customer vanished from the documents and from the graph. The renamed one showed up under its new name.

That is the behaviour I wanted.

The bug only a real account could find

My first real run printed every charge without a link to its invoice.

My fake Stripe gave charges an invoice field. The real one does not. In the API version I pin (2025-03-31) a charge no longer says which invoice it paid. Instead the invoice lists its payments, and you have to ask for them. My tests passed on imaginary data and missed it completely.

The fix links the two through the payment they share. The invoice and the charge now both belong to the same payment_intent node. The fake now behaves like the real thing, so a test would catch this next time. A review pass flagged a second risk. A dispute's events name only the dispute, so the charge's disputed flag could go stale. The connector now refreshes the charge too.

Try it

The seed script and the example live in packages/connector/stripe/. Stripe's test mode is free and needs no business verification. Create a restricted key with read access, run the seed once, then rename and delete things in the dashboard between runs.

If you point it at your own account I would love to hear what breaks.

Top comments (0)