Store two dates with every response. The first date is the date of your request. The second date is the date when the vendor read the data, if the response gives it. Ask again on a schedule that suits the task. Keep the files, not the links. Maintained context is context prepared from sources added for the brand. For maintained context, store the version ID with the work that it produced.
| Endpoint | How the response says how old it is | What the docs say about age and storage |
|---|---|---|
| Brandfetch Brand Context API | meta.resolved_at |
Brandfetch returns a stored profile until it refreshes the profile. Brandfetch does not publish how often. The terms allow caching only by written agreement, for up to 30 days |
| Brandfetch Brand API | Not stated | The frequently asked questions on the pricing page say that you can cache responses for 30 days. You can cache shortcut image addresses with no time limit |
| Context.dev Brand and Styleguide | cache_metadata.age_ms |
Context.dev uses a cached copy up to maxAgeMs, about 90 days by default. 0 reads the site again |
| Firecrawl branding | Not stated | By default, Firecrawl uses a cached copy up to 2 days old (maxAge). 0 reads the page again. Firecrawl charges a cached result like a fresh one |
| Logo.dev Brand API | data.indexed_at, etag per file |
You can store anything that it returns. File links expire after about 24 hours. Compare etag to find out if an image changed |
| Rainbrand Brief | skillId |
The Brand Skill version that the Brief came from. After you apply a brand edit, request a new Brief |
| Rainbrand Brand Skill | skillId (REST), version_id (Model Context Protocol, MCP) |
Each version is a snapshot that does not change |
A cache that keeps both dates
type Cached<T> = { value: T; requestedAt: string; readAt: string | null };
const store = new Map<string, Cached<unknown>>();
export async function cached<T>(
key: string,
maxAgeDays: number,
load: () => Promise<{ value: T; readAt: string | null }>
): Promise<Cached<T>> {
const hit = store.get(key) as Cached<T> | undefined;
const age = hit ? (Date.now() - Date.parse(hit.readAt ?? hit.requestedAt)) / 86_400_000 : Infinity;
if (hit && age < maxAgeDays) return hit;
const { value, readAt } = await load();
const entry = { value, requestedAt: new Date().toISOString(), readAt };
store.set(key, entry);
return entry;
}
// Age is counted from when Brandfetch read the site, not from when you asked.
// const ctx = await cached(`brandfetch-context:${domain}`, 30, async () => {
// const c = await getContext(domain);
// return { value: c, readAt: c.meta.resolved_at };
// });
import time
from datetime import datetime, timezone
_store: dict[str, dict] = {}
def cached(key: str, max_age_days: float, load):
hit = _store.get(key)
if hit:
stamp = datetime.fromisoformat((hit["read_at"] or hit["requested_at"]).replace("Z", "+00:00"))
if (time.time() - stamp.timestamp()) / 86400 < max_age_days:
return hit
value, read_at = load()
hit = {"value": value, "requested_at": datetime.now(timezone.utc).isoformat(), "read_at": read_at}
_store[key] = hit
return hit
Brandfetch read the Linear profile on 16 October and returned it on 11 November. The profile was 26 days old when we got it. If you count a 30-day cache from the request, the cache keeps the profile until 11 December.
Recording which context a piece of work used
Keep the source and the version of the context next to the work that it produced. For a Rainbrand Brief, keep skillId. Observed context is context read from a company's public website. For observed context, keep the endpoint and the read date.
await saveOutput({
output,
brandContext: { endpoint: "Rainbrand Brief", briefId: brief.data.id, version: brief.data.skillId },
});
If a customer asks why last month's ad used the old tagline, this record gives you the answer.
Last updated 11 November 2026