Guides / Use cases
Shopping assistants
For shopping assistants and recommendation chats in a web shop or app. Recall remembers each shopper's sizes, tastes, budget and returns across visits, and hands your catalogue search their preferences as structured filters, so recommendations fit the first time.
What memory adds
Without memory, every visit starts with the same questions. With it, the assistant already knows what fits:
| Without memory | With Recall | |
|---|---|---|
| "Trail shoes for muddy runs, nothing neon, max €150" | Asks for the size, then shows the bestseller in neon green. | Shows the Trailblaze GTX in black, in 43: the 42s came back too small. |
| "Something to eat on long runs?" | Suggests whey protein bars. | Only vegan bars: the shopper's account says vegan. |
| Next month, a rain jacket | Starts again from zero. | Keeps to the budget and the dislike of neon without being told again. |
Recall does not search your catalogue or write the replies. Your search and your model do; Recall tells them what this shopper wants, and learns from what they say.
How to model it
| In your shop | In Recall | Why |
|---|---|---|
| Your shop | Workspace, e.g. shop-eu | One memory space for all shoppers. Use separate workspaces only where data must stay apart, such as separate brands or legal entities. |
| A shopper | Peer cust-1042, your customer id | Recall learns from what they write in chat and in return forms. |
| Your assistant | Peer shop-assistant with observeMe: false | Your product pitches are not facts about anyone; its replies cost less. |
| A chat visit | Session, e.g. chat-5e21c | A summary per visit; what was learned about the shopper carries over. |
| An order with a return | Session, e.g. order-77120, with dated messages | The shopper's return reason is first-hand information about fit. |
| Sizes and diets the shopper entered in their account | Conclusions you create | Facts as given, stored at once; nothing is inferred. |
| A shopper who opts out of personalisation | observeMe: false on their peer | Recall stops learning about them. |
Build it
Install the SDK (npm install @recall-memory/sdk or pip install recall-mem) and set RECALL_API_KEY. Create the shopper and the assistant, and tell Recall once what to learn in your shop and what never to record. Custom instructions guide the model that extracts facts; they are not a hard filter, so also keep sensitive data out of what you send.
import { Recall } from "@recall-memory/sdk";
const recall = new Recall({ workspaceId: "shop-eu" });
// The shopper is a peer keyed by your customer id; the assistant is a peer Recall does not
// learn about.
const shopper = recall.peer("cust-1042", { metadata: { country: "NL" } });
const assistant = recall.peer("shop-assistant", { configuration: { observeMe: false } });
// Once, for the whole shop: what Recall should learn here, and what it must not.
await recall.setConfiguration({
reasoning: {
customInstructions:
"Record sizes and fit per brand, budgets, colours, styles, brands, diets the shopper " +
"states, and returns with their reasons. Never record or guess health, pregnancy, " +
"religion, ethnicity, sexuality or money trouble from purchases or messages.",
},
});
from pydantic import BaseModel
from recall_memory import PeerConfig, Recall
recall = Recall(workspace_id="shop-eu")
# The shopper is a peer keyed by your customer id; the assistant is a peer Recall does not
# learn about.
shopper = recall.peer("cust-1042", metadata={"country": "NL"})
assistant = recall.peer("shop-assistant", configuration=PeerConfig(observe_me=False))
# Once, for the whole shop: what Recall should learn here, and what it must not.
recall.set_configuration(
{
"reasoning": {
"custom_instructions": (
"Record sizes and fit per brand, budgets, colours, styles, brands, diets the "
"shopper states, and returns with their reasons. Never record or guess health, "
"pregnancy, religion, ethnicity, sexuality or money trouble from purchases or "
"messages."
)
}
}
)
Account data: facts you write. Sizes and diets the shopper entered in their account are things they told you, not guesses. Write them as conclusions: stored as given and usable at once. Preferences that come up in chat ("nothing neon") are learned from the messages in the background, dated, and updated when they change.
// What the shopper entered in their account is fact, not inference: store it as facts.
await shopper.conclusions.create(["Shoe size EU 42.", "Eats vegan."]);
# What the shopper entered in their account is fact, not inference: store it as facts.
shopper.conclusions.create([{"content": "Shoe size EU 42."}, {"content": "Eats vegan."}])
Returns: the shopper's words, dated. A return reason is some of the best information you have about fit. Add the text from the returns form as the shopper's message, in a session for the order, with createdAt set to when it was filed. Recall learns from it like any other message, so the next recommendation knows the 42s were too small in this brand. Put order numbers and SKUs in metadata for your own use; Recall learns from the text.
// A return from an earlier order, in the shopper's words from the returns form, dated when
// it was filed. Recall learns from it like any message, and can close facts it contradicts.
const order = recall.session("order-77120");
await order.addMessages([
shopper.message("Returning the Trailblaze GTX in 42: too small. I need a 43 in this brand.", {
createdAt: "2026-10-08T17:20:00Z",
metadata: { order: "77120", sku: "TB-GTX-42", reason: "too_small" },
}),
]);
# A return from an earlier order, in the shopper's words from the returns form, dated when
# it was filed. Recall learns from it like any message, and can close facts it contradicts.
order = recall.session("order-77120")
order.add_messages(
[
shopper.message(
"Returning the Trailblaze GTX in 42: too small. I need a 43 in this brand.",
created_at="2026-10-08T17:20:00Z",
metadata={"order": "77120", "sku": "TB-GTX-42", "reason": "too_small"},
)
]
)
In the chat: one request per message. turn stores the shopper's message and returns the context for the reply: the visit so far and what Recall knows about the shopper, including the account facts and the return. Building the context uses no model and takes 0.3 to 0.7 seconds. Your model writes the reply from it and from your catalogue.
// In the shop's chat: one request stores the message and returns what Recall knows about
// the shopper for the reply. Preferences stated here are learned in the background.
const visit = recall.session("chat-5e21c");
const { context } = await visit.turn(
[shopper.message("Trail shoes for muddy autumn runs, please. Nothing neon, max €150.")],
{ peerTarget: shopper, tokens: 2000 },
);
const prompt = context.toOpenAI(assistant);
// Your model writes the reply from `prompt` and your catalogue; store it too.
const reply = "The Trailblaze GTX in black grips well in mud, €139. In 43 this time?";
await visit.addMessages([assistant.message(reply)]);
# In the shop's chat: one request stores the message and returns what Recall knows about
# the shopper for the reply. Preferences stated here are learned in the background.
visit = recall.session("chat-5e21c")
result = visit.turn(
[shopper.message("Trail shoes for muddy autumn runs, please. Nothing neon, max €150.")],
peer_target=shopper,
tokens=2000,
)
prompt = result.context.to_openai(assistant=assistant)
# Your model writes the reply from `prompt` and your catalogue; store it too.
reply = "The Trailblaze GTX in black grips well in mud, €139. In 43 this time?"
visit.add_messages([assistant.message(reply)])
Preferences as filters. Your search needs fields, not prose. Ask for JSON with responseFormat once per visit and keep the result in the shopper's web session: a question takes 2 to 4.5 seconds and each one is billed. Then filter or rank results in your own search. Make fields nullable so "not known" never filters out everything, and let filters the shopper sets in your UI win over memory.
// Turn memory into filters for your catalogue: ask for JSON once per visit and keep it in your
// session. (waitForIdle is for this demo; real apps rarely block on learning.)
await recall.waitForIdle();
type Filters = {
shoe_size_eu: number | null;
max_price_eur: number | null;
avoid_colours: string[];
diet: string[];
};
const prefs = await shopper.chat("Which filters should a product search use for this shopper?", {
reasoningLevel: "low",
responseFormat: {
jsonSchema: {
type: "object",
properties: {
shoe_size_eu: { type: ["number", "null"] },
max_price_eur: { type: ["number", "null"] },
avoid_colours: { type: "array", items: { type: "string" } },
diet: { type: "array", items: { type: "string" } },
},
required: ["shoe_size_eu", "max_price_eur", "avoid_colours", "diet"],
},
parse: (data: unknown) => data as Filters, // or a zod schema's parse
},
});
const catalogue = [
{ sku: "TB-GTX-43", size: 43, price: 139, colour: "black" },
{ sku: "VX-9-43", size: 43, price: 165, colour: "neon green" },
];
const picks = catalogue.filter(
(p) =>
(!prefs?.max_price_eur || p.price <= prefs.max_price_eur) &&
!prefs?.avoid_colours.some((c) => p.colour.includes(c)),
);
# Turn memory into filters for your catalogue: ask for JSON once per visit and keep it in your
# session. (wait_for_idle is for this demo; real apps rarely block on learning.)
recall.wait_for_idle()
class Filters(BaseModel):
shoe_size_eu: float | None
max_price_eur: float | None
avoid_colours: list[str]
diet: list[str]
prefs = shopper.chat(
"Which filters should a product search use for this shopper?",
reasoning_level="low",
response_format=Filters,
)
catalogue = [
{"sku": "TB-GTX-43", "size": 43, "price": 139, "colour": "black"},
{"sku": "VX-9-43", "size": 43, "price": 165, "colour": "neon green"},
]
picks = [
p
for p in catalogue
if prefs is None
or (
(not prefs.max_price_eur or p["price"] <= prefs.max_price_eur)
and not any(c in p["colour"] for c in prefs.avoid_colours)
)
]
Opting out. When a shopper turns personalisation off, set observeMe: false on their peer: Recall stops learning from their messages. Delete what was learned and clear their card. Their chats stay stored for the conversation itself. To erase a shopper completely, for example when they close their account, call shopper.delete().
// The shopper turns personalisation off: stop learning from their messages and drop every
// fact memory holds about them. Their chats stay stored.
// (To erase the shopper completely instead: await shopper.delete())
await shopper.setConfiguration({ observeMe: false });
const learned = await (await shopper.conclusions.list()).toArray();
for (const fact of learned) await shopper.conclusions.delete(fact.id);
await shopper.setCard([]);
# The shopper turns personalisation off: stop learning from their messages and drop every
# fact memory holds about them. Their chats stay stored.
# (To erase the shopper completely instead: shopper.delete())
shopper.set_configuration(PeerConfig(observe_me=False))
learned = list(shopper.conclusions.list())
for fact in learned:
shopper.conclusions.delete(fact.id)
shopper.set_card([])
Settings that matter
| Setting | Use |
|---|---|
reasoning.customInstructions (workspace configuration) | What to learn in your shop, and what never to record. A guide for the model, not a guarantee. |
observeMe: false | On your assistant, always. On shoppers who opt out. |
responseFormat | Filters as JSON: an object at the root, nullable fields for "not known". |
reasoningLevel | low (the default) for filters; minimal is cheaper and faster for a few simple fields. See Reasoning levels and models. |
Message createdAt | Returns and imported chats: when each was written, so the newest information wins. |
peerTarget and tokens on turn | peerTarget: shopper adds what Recall knows about them; 1,500 to 3,000 tokens is plenty for a shopping chat. |
Cost and speed
Measured from Europe against the hosted API (Frankfurt), October 2026: a turn or context call took 0.3 to 0.7 seconds, a search about 0.9 seconds, a question at minimal or low 2 to 4.5 seconds. Learning happens in the background, usually within about 20 seconds of a message, and never holds up a call.
Context, search, storage, summaries and dreams are included. You pay for learning, $1.75 per million message tokens with every message counting as at least 50, and for questions. A shopper who chats twice a month (6 messages of about 30 tokens and 6 replies of about 120 tokens each time), files one return, and gets filters computed once per visit at low:
| Item | Per month |
|---|---|
| Shopper messages and the return: 13 × 50 tokens × $1.75 / 1M | $0.0011 |
| Assistant replies, read but not learned from: 12 × 120 tokens × $1.75 / 1M | $0.0025 |
Filters at low: 2 × $0.005 | $0.0100 |
| Total | about $0.014 per shopper |
Questions are most of the bill: a cached answer is still billed as one query, so compute filters once per visit, not on every page view. More in Cost and speed.
Privacy and forgetting
What people buy and say while shopping can reveal more than their size. Be clear about what the assistant remembers, and keep it to shopping.
- Tell shoppers. Say that the assistant remembers sizes, preferences and returns from chats and orders, give them a switch to turn it off, and a page that shows what is remembered.
- Do not infer sensitive traits. Purchases hint at health, pregnancy, religion or sexuality. Do not record such guesses or act on them: the custom instructions above help, and so does not sending order history you do not need. Health and similar data have stricter rules under the GDPR; see Privacy, consent and forgetting (this is not legal advice).
- Keep out what memory does not need: payment details and full addresses. Your shop already holds them.
- Marketing is a separate purpose. Using what the assistant remembers in newsletters or ads goes beyond helping the shopper in the chat; check what your privacy notice and consent cover.
- Forgetting:
shopper.delete()(orDELETE /v3/workspaces/{workspace}/peers/{peer}) erases the shopper's messages, every fact and card about them, the summaries that quoted them and cached answers. See Forget a person. - Where data lives: the database and API are in Frankfurt (EU). Text is processed by AI model providers in the US through OpenRouter, restricted to providers that do not collect the data, and customer data is not used to train models. See the privacy policy and the DPA.
Pitfalls
- Inferring sensitive traits. "Bought prenatal vitamins" must never become "is pregnant". Tell Recall not to record such things, and do not send purchases it does not need.
- Gifts read as taste. A child's raincoat bought once says nothing about the shopper's size. Do not write every purchase down as a fact; let memory come from what the shopper says.
- Asking on every page view. A question takes seconds and each one is billed. Compute filters once per visit and keep them in your session.
- Memory over explicit choices. If the shopper filters for size 44, show 44. Memory fills gaps; it does not overrule what they just asked for.
- One size for every brand. Fit differs per brand. Keep the return reasons, and ask for the size in the brand you are about to show rather than one number for everything.
- Learning from your own pitches. Without
observeMe: falseon the assistant, "grips well in mud" is learned, and billed, as a statement about the assistant.
Next: Personal assistants and companions · Privacy, consent and forgetting