Gift ideas
Describe the person and a budget, and get gift ideas found on real gift guides and reviews, each with why it suits them, a price range when the page gives one, and the page it came from. Ideas over the budget are left out.
When to use it: You want gift ideas for a person and a budget, from real gift guides.
| Uses | search, extract over the top pages |
|---|---|
| Needs | web search and a model on the API (no keys of your own) |
| Site | whatever the search finds (gift guides, reviews) |
| Output | output/ideas.md, output/result.json |
| Languages | Node and Python |
No shop is scraped: the search goes through the platform's search provider, and the ideas come from the pages it finds. An idea kept must name a page that was read, and one whose lowest price is over the budget (in the same currency) is dropped.
The code
The example as it is in its folder. Both versions do the same job and write the same output/, so one result check covers both.
/** * Gift ideas: describe the person and a budget, and get gift ideas found on real gift guides and reviews, each with * why it suits them, a price range, and the page it came from. Ideas over the budget are left out. * * npx tsx node/index.ts (BOXLINE_API_KEY; RECIPIENT, BUDGET, CURRENCY) * * One search, then one extract over the top pages. Writes output/result.json and output/ideas.md. */import { mkdirSync, writeFileSync } from "node:fs";import { join } from "node:path";import { Boxline } from "@boxline/sdk";const out = process.env.OUTPUT_DIR ?? "output";const recipient = process.env.RECIPIENT ?? "a friend in their thirties who loves rock climbing and good coffee";const budget = Number(process.env.BUDGET ?? 60);const currency = process.env.CURRENCY ?? "USD";const bx = new Boxline();type Idea = { idea: string; why: string; priceMin: number | null; priceMax: number | null; currency: string | null; source: string };// 1. Gift guides and reviews for this person (the platform's search; no shop is scraped).const found = await bx.search({ query: `gift ideas for ${recipient} under ${budget} ${currency}`, limit: 8 });const urls = found.results.map((r) => r.url).slice(0, 5);console.log(`Reading ${urls.length} pages:\n${urls.map((u) => ` ${u}`).join("\n")}`);if (!urls.length) throw new Error("the search found nothing; describe the person differently");// 2. Ideas from those pages, each tied to the person and to its page.const r = await bx.extract<{ ideas: Idea[] }>({ urls, prompt: `Gift ideas for ${recipient}, with a budget of ${budget} ${currency}. From these pages only: 6 to 10 ideas, the most fitting first. ` + `why: one sentence on why it suits this person. priceMin / priceMax: the price range the page gives (null when it gives none). source: the URL of the page it is from.`, schema: { type: "object", properties: { ideas: { type: "array", items: { type: "object", properties: { idea: { type: "string" }, why: { type: "string" }, priceMin: { type: ["number", "null"] }, priceMax: { type: ["number", "null"] }, currency: { type: ["string", "null"] }, source: { type: "string" } }, required: ["idea", "why", "priceMin", "priceMax", "currency", "source"] }, }, }, required: ["ideas"], },});const read = new Set(r.pages.filter((p) => !p.error && (p.status ?? 0) < 400).flatMap((p) => [p.url, p.finalUrl]));const strip = (u: string) => u.replace(/[#?].*$/, "").replace(/\/$/, "");const known = new Set([...read].filter(Boolean).map((u) => strip(u!)));// Kept: ideas from a page that was read, within the budget when a price is given (in the same currency).const ideas = r.data.ideas.filter((i) => known.has(strip(i.source)) && !(i.priceMin !== null && (i.currency ?? currency) === currency && i.priceMin > budget));console.log(`\n${ideas.length} ideas within ${budget} ${currency} (of ${r.data.ideas.length} found):`);for (const i of ideas) console.log(` • ${i.idea}${i.priceMin !== null ? ` (${i.priceMin}${i.priceMax && i.priceMax !== i.priceMin ? `–${i.priceMax}` : ""} ${i.currency ?? currency})` : ""}\n ${i.why}\n ${i.source}`);const md = [`# Gift ideas for ${recipient}`, "", `Budget: ${budget} ${currency}.`, "", ...ideas.map((i) => `- **${i.idea}**${i.priceMin !== null ? ` (${i.priceMin}${i.priceMax && i.priceMax !== i.priceMin ? `–${i.priceMax}` : ""} ${i.currency ?? currency})` : ""}: ${i.why} ([source](${i.source}))`), ""].join("\n");mkdirSync(out, { recursive: true });writeFileSync(join(out, "ideas.md"), md);writeFileSync(join(out, "result.json"), JSON.stringify({ recipient, budget, currency, pages: r.pages, ideas, dropped: r.data.ideas.length - ideas.length, usage: { modelUsd: r.usage.costUsd, searches: found.cached ? 0 : 1 } }, null, 2));"""Gift ideas: describe the person and a budget, and get gift ideas found on real gift guides and reviews, each withwhy it suits them, a price range, and the page it came from. Ideas over the budget are left out. python python/main.py (BOXLINE_API_KEY; RECIPIENT, BUDGET, CURRENCY)One search, then one extract over the top pages. Writes output/result.json and output/ideas.md."""import jsonimport osimport refrom pathlib import Pathfrom boxline import Boxlineout = Path(os.environ.get("OUTPUT_DIR", "output"))recipient = os.environ.get("RECIPIENT", "a friend in their thirties who loves rock climbing and good coffee")budget = float(os.environ.get("BUDGET", "60"))currency = os.environ.get("CURRENCY", "USD")bx = Boxline()# 1. Gift guides and reviews for this person (the platform's search; no shop is scraped).found = bx.search(f"gift ideas for {recipient} under {budget:g} {currency}", limit=8)urls = [x["url"] for x in found["results"]][:5]print(f"Reading {len(urls)} pages:\n" + "\n".join(f" {u}" for u in urls))if not urls: raise SystemExit("the search found nothing; describe the person differently")# 2. Ideas from those pages, each tied to the person and to its page.r = bx.extract( urls=urls, prompt=( f"Gift ideas for {recipient}, with a budget of {budget:g} {currency}. From these pages only: 6 to 10 ideas, the most fitting first. " "why: one sentence on why it suits this person. priceMin / priceMax: the price range the page gives (null when it gives none). source: the URL of the page it is from." ), schema={ "type": "object", "properties": { "ideas": { "type": "array", "items": {"type": "object", "properties": {"idea": {"type": "string"}, "why": {"type": "string"}, "priceMin": {"type": ["number", "null"]}, "priceMax": {"type": ["number", "null"]}, "currency": {"type": ["string", "null"]}, "source": {"type": "string"}}, "required": ["idea", "why", "priceMin", "priceMax", "currency", "source"]}, } }, "required": ["ideas"], },)def strip(u): return re.sub(r"[#?].*$", "", u or "").rstrip("/")known = {strip(u) for p in r["pages"] if not p.get("error") and (p.get("status") or 0) < 400 for u in (p["url"], p.get("finalUrl")) if u}# Kept: ideas from a page that was read, within the budget when a price is given (in the same currency).ideas = [i for i in r["data"]["ideas"] if strip(i["source"]) in known and not (i["priceMin"] is not None and (i["currency"] or currency) == currency and i["priceMin"] > budget)]def price(i): if i["priceMin"] is None: return "" upper = f"–{i['priceMax']:g}" if i["priceMax"] and i["priceMax"] != i["priceMin"] else "" return f" ({i['priceMin']:g}{upper} {i['currency'] or currency})"print(f"\n{len(ideas)} ideas within {budget:g} {currency} (of {len(r['data']['ideas'])} found):")for i in ideas: print(f" • {i['idea']}{price(i)}\n {i['why']}\n {i['source']}")md = [f"# Gift ideas for {recipient}", "", f"Budget: {budget:g} {currency}.", ""] + [f"- **{i['idea']}**{price(i)}: {i['why']} ([source]({i['source']}))" for i in ideas] + [""]out.mkdir(parents=True, exist_ok=True)(out / "ideas.md").write_text("\n".join(md))result = {"recipient": recipient, "budget": budget, "currency": currency, "pages": r["pages"], "ideas": ideas, "dropped": len(r["data"]["ideas"]) - len(ideas), "usage": {"modelUsd": r["usage"]["costUsd"], "searches": 0 if found["cached"] else 1}}(out / "result.json").write_text(json.dumps(result, indent=2, ensure_ascii=False))Inputs
Environment variables the example reads:
RECIPIENT,BUDGET(default 60),CURRENCY(default USD).
Run it
Get the examples with git clone https://github.com/Boxline-dev/examples.git and open this one: cd examples/gift-ideas. Put your API key in BOXLINE_API_KEY (and BOXLINE_API_URL for another API), then run either version from the example’s folder. Both write to output/ and stop their sessions when they finish.
# Node 18 or newer, in the example's folderexport BOXLINE_API_KEY=… # your API keynpm install @boxline/sdk tsxnpx tsx node/index.ts# Python 3.9 or newer, in the example's folderexport BOXLINE_API_KEY=… # your API keypip install boxline-sdkpython python/main.pyThen check the result, from the same folder: npx tsx check.ts output. It exits with 0 only when the result is right.
What its check proves
check.ts reads what the example wrote and passes only when the result is right, not merely when the example ran.
There are at least 3 ideas, each with a reason and from a page that was read, and none starts over the budget.
Source
github.com/Boxline-dev/examples/gift-ideas on GitHub, with its README.md, check.ts and .env.example. The examples are open source. See all examples.
These docs describe a beta API. Endpoints, fields and SDK methods may change before general availability; breaking changes will be listed in the changelog.