Let a guarded book agent create a Stripe test-mode checkout link, but only after explicit user approval.
This cookbook builds on the guarded memory agent from earlier recipes in the same arc and adds one production capability: the agent can propose an external action. The model never gets uncontrolled authority. It can request a checkout action, but the FastAPI service owns approval, policy, audit events, and the Stripe MCP call.
What you will build
A FastAPI service that extends the guarded memory agent with:
- The inherited agent stack. LangGraph routing, thread memory, Postgres user memory, LangSmith tracing, guardrails, server-sent events, and metrics stay in place.
- A seeded book catalog. Ten fictional books live in
scripts/data/books.json, with illustration paths ready for your assets. - A Stripe setup script.
make seed-stripe-bookscreates Stripe test-mode Products and Prices through the Stripe REST API and writesdata/stripe_books.json. - A remote Stripe MCP runtime. Approved actions call
https://mcp.stripe.comwith thecreate_payment_linktool. - Human approval.
/agent/runemits anapproval_requiredevent, and/approvals/{approval_id}approves or rejects the side effect.
book purchase prompt
└─► input guardrails
└─► select seeded book
└─► create pending approval
├─► reject: no Stripe call
└─► approve: Stripe MCP create_payment_link
Prerequisites
- Python 3.12 or newer
uvinstalled- A Nebius API key
- A Stripe Sandbox or test-mode account
- A Stripe test-mode secret key for seeding
- A restricted Stripe key for remote MCP runtime calls
- Docker, for local Postgres
Run the cookbook
cd cookbooks/09-actions-with-mcp-stripe
uv sync
cp .env.example .env
Fill these values in .env:
NEBIUS_API_KEY=...
NEBIUS_BASE_URL=https://api.tokenfactory.nebius.com/v1/
NEBIUS_MODEL=Qwen/Qwen3.5-397B-A17B
STRIPE_SECRET_KEY=sk_test_or_sandbox_seed_key
STRIPE_MCP_API_KEY=rk_test_or_sandbox_restricted_key
The two NEBIUS_ overrides matter: the repo’s .env.example still ships the older api.studio.nebius.ai hostname and a chat model that Token Factory no longer serves, so the first request fails without them.
Start Postgres for the inherited long-term memory:
docker compose up -d postgres
Create the Stripe test catalog:
make seed-stripe-books
Run the app:
make dev
In the web playground, open this recipe and ask:
I want to buy The Nebius Cloud Atlas.
The assistant emits an approval card. Approving it calls Stripe MCP and returns a test checkout URL.
Environment
| Variable | Default | Purpose |
|---|---|---|
NEBIUS_API_KEY |
required | Nebius API key. |
STRIPE_SECRET_KEY |
sk_test_replace_me |
Setup-only key used by make seed-stripe-books. |
STRIPE_MCP_API_KEY |
rk_test_replace_me |
Runtime key used to call remote Stripe MCP. |
STRIPE_MCP_BASE_URL |
https://mcp.stripe.com |
Remote Stripe MCP endpoint. |
BOOK_CATALOG_PATH |
data/stripe_books.json |
Generated catalog with Stripe Product and Price IDs. |
APPROVAL_TTL_SECONDS |
900 |
Time window for approving a pending action. |
MEMORY_BACKEND |
postgres |
Inherited long-term memory backend. |
POSTGRESQL_ADDON_URI |
local Postgres URL | Inherited memory database. |
LANGSMITH_TRACING |
false |
Enables LangSmith SaaS traces when credentials are configured. |
NEBIUS_BASE_URL and NEBIUS_MODEL are not in this table because the quickstart already tells you to set them: the repo’s .env.example and config default still point at the retired api.studio.nebius.ai host and model.
Walk-through
Stripe MCP and sandboxes
Stripe MCP exposes Stripe API operations as MCP tools, including create_payment_link. This cookbook uses the remote MCP endpoint instead of a local npx @stripe/mcp subprocess, so the Python service stays simple to run and easy to mock in tests.
Use a Stripe Sandbox or test-mode account. Never use live keys for this cookbook.
Stripe Sandboxes are newer than the classic global test-mode toggle. A Sandbox is an isolated Stripe environment with its own API keys, objects, settings, and test data, which is useful for agent work: you can give the agent a realistic account shape without letting it touch your real Stripe account or pollute a shared test-mode setup.
Classic test mode is still enough for local experimentation. A Sandbox is the stronger choice for repeatable demos, CI smoke tests, or multiple isolated agent environments. The recommended setup is:
- one Sandbox dedicated to the cookbook demo;
- one setup key for creating the fake Products and Prices;
- one restricted runtime key for the remote MCP server;
- no live-mode objects, prices, customers, or checkout links.
This separation mirrors a production control boundary. Operators prepare the commerce catalog, and the agent can only request a narrow runtime action after the user approves it.
Recommended key model:
- Use
STRIPE_SECRET_KEYonly for the operator seed step. - Use
STRIPE_MCP_API_KEYfor the running app. - Prefer a restricted key for MCP that can create Payment Links from existing Prices and cannot perform broad account operations.
- Rotate both keys after demos or CI smoke tests.
The seed script uses Stripe REST instead of MCP on purpose. Seeding is an operator task: create Products and multi-currency Prices once, then let the runtime agent use MCP only for the approved action. Each book gets one Stripe Price with USD as the default currency and EUR, GBP, and SGD configured through Stripe currency_options.
Why Stripe MCP here
Stripe is a good action for this kind of cookbook because it is concrete, high stakes, and easy to keep safe in test mode. Creating a checkout link is visibly different from answering a question, and the approval pattern stays clear: payment-related side effects should never happen just because the model suggested them.
Other MCP servers could participate in the same flow:
- Catalog or CMS MCP to fetch approved book metadata before checkout.
- Inventory MCP to confirm a book or bundle is available.
- CRM MCP to associate the approved action with a demo user or account.
- Email MCP to send the checkout link after approval.
- Ticketing or audit MCP to record the approval trail for compliance.
- Knowledge-base MCP to fetch refund, tax, or shipping policy before the agent explains the checkout.
The pattern stays the same regardless of the MCP provider:
model proposes action → backend validates policy → user approves → MCP tool executes
Tool discovery is not tool authority. MCP gives the agent a standardized way to reach tools, but the application still decides which tools are allowed, what arguments are valid, who can approve them, and what gets logged.
Seed catalog
The source catalog is committed at scripts/data/books.json. It contains ten fictional books that get created in the Stripe Sandbox. Each book uses a single Stripe Price with USD as the default and EUR, GBP, and SGD as multi-currency options:
| Book | Author | ISBN-13 | USD | EUR | GBP | SGD |
|---|---|---|---|---|---|---|
The Nebius Cloud Atlas |
Nia Vector | 9781600000010 |
$14.99 | €13.99 | £11.99 | S$19.99 |
Pinecones in the Vector Garden |
Ada Embedding | 9781600000027 |
$12.99 | €11.99 | £10.99 | S$17.99 |
Agent at the End of the Prompt |
Lila Chain | 9781600000034 |
$10.99 | €9.99 | £8.99 | S$14.99 |
The Checkout Graph |
Max Token | 9781600000041 |
$15.99 | €14.99 | £12.99 | S$21.99 |
Embeddings & Espresso |
Clara Context | 9781600000058 |
$13.99 | €12.99 | £10.99 | S$18.99 |
The Retrieval Society |
Jonas Index | 9781600000065 |
$16.99 | €15.99 | £13.99 | S$22.99 |
Guardrails for Dreaming Machines |
Mira Policy | 9781600000072 |
$9.99 | €8.99 | £7.99 | S$13.99 |
Ten Thousand Tiny Agents |
Theo Router | 9781600000089 |
$14.99 | €13.99 | £11.99 | S$19.99 |
The Sandbox Bookshop |
Samira Stripe | 9781600000096 |
$12.99 | €11.99 | £10.99 | S$17.99 |
When the Model Learned to Read |
Lucie Latency | 9781600000102 |
$11.99 | €10.99 | £9.99 | S$15.99 |
Each ISBN is fictional but carries a valid ISBN-13 check digit, and is added to Stripe Product and Price metadata as isbn. The seed script uses that ISBN to create deterministic Stripe Product IDs in the format nebius_partners_book_<isbn>. For example, The Nebius Cloud Atlas is created as nebius_partners_book_9781600000010.
Each item includes a cover_image_path alongside books.json. Stripe Product images are public URLs rather than local file uploads: if you set STRIPE_IMAGE_BASE_URL, the seed script sends images[0] for each Stripe Product by joining that public base URL with the cover filename. If data/stripe_books.json already exists, rerunning the seed script with STRIPE_IMAGE_BASE_URL set backfills the image URL on each existing Stripe Product without creating new Products or Prices. If an older generated catalog contains random prod_... Product IDs, rerunning the seed script creates or updates the deterministic nebius_partners_book_<isbn> Products and writes a fresh catalog.
The generated catalog lives at data/stripe_books.json. It is gitignored because Stripe Price IDs are account-specific even though the Product IDs are deterministic; the Price IDs are the runtime handles passed to Stripe MCP when creating a Payment Link.
Useful commands:
uv run python scripts/seed_stripe_books.py --dry-run
uv run python scripts/seed_stripe_books.py --force
API
POST /agent/run handles normal book prompts through the guarded Nebius agent. Purchase prompts create a pending approval instead of calling Stripe.
curl -N -X POST http://localhost:8000/agent/run \
-H 'content-type: application/json' \
-d '{"thread_id":"checkout-demo","user_id":"reader-42","prompt":"I want to buy The Nebius Cloud Atlas."}'
Relevant SSE events:
event: status
data: {"phase":"input_guardrail","rule":"all","outcome":"passed"}
event: approval_required
data: {"approvalId":"...","action":"stripe.create_payment_link","book":{"title":"The Nebius Cloud Atlas","amount":1499,"currency":"usd"}}
event: answer
data: {"text":"I can create a Stripe test-mode checkout link ... but I need your approval first."}
POST /approvals/{approval_id} approves or rejects the pending side effect:
curl -X POST http://localhost:8000/approvals/$APPROVAL_ID \
-H 'content-type: application/json' \
-d '{"decision":"approve"}'
curl -X POST http://localhost:8000/approvals/$APPROVAL_ID \
-H 'content-type: application/json' \
-d '{"decision":"reject"}'
Approvals are process-local in this recipe. Move them to Postgres or Redis before using this pattern across replicas.
Design decisions
The approval gate is the core safety mechanism. app/core/approvals.py keeps an in-memory ApprovalStore keyed by a generated approval ID, with a status of pending, completed, rejected, or expired. Reading a record checks its expiry first, so a request against a stale approval is marked expired before any decision is processed. _require_pending refuses to reject or complete anything that is not currently pending, which makes the endpoint idempotent: calling /approvals/{approval_id} again on an already completed or rejected record returns that final state instead of taking a second action. The Stripe MCP call in app/routes/approvals.py only happens inside the approve branch, after that pending check passes.
Use a Stripe Sandbox or test-mode key (sk_test_, rk_test_) and never a live key. Test mode is not enforced in code. app/config.py defaults stripe_mcp_api_key and stripe_secret_key to placeholder strings that look like test keys (sk_test_replace_me, rk_test_replace_me), and the seed script calls the same https://api.stripe.com/v1 endpoint that Stripe uses for both test and live keys. Nothing validates the key prefix at startup or before a call. Safety depends entirely on the operator following the README’s instruction to use a Stripe Sandbox or test-mode account and never a live key.
Failure modes
- Approval expiry.
APPROVAL_TTL_SECONDS(default 900) bounds how long a pending approval stays valid. An expired approval returns HTTP 410 rather than silently falling through to Stripe. - Missing Stripe Price ID.
StripeMCPClient.create_payment_linkraises before calling Stripe if the selected book has nostripe_price_id, which happens ifmake seed-stripe-bookswas never run or targeted a different catalog file. - Stripe MCP errors. Any HTTP error or JSON-RPC error from
https://mcp.stripe.comis wrapped inStripeMCPErrorand surfaced as HTTP 502, not swallowed. - Missing local cover images. The seed script refuses to run if a book’s
cover_image_pathdoes not exist on disk next tobooks.json, unless in--dry-runmode.
Test it
make test
make lint
The test suite covers:
- guarded normal book answers
- non-book refusal
- prompt-injection refusal
- PII redaction
- approval creation without Stripe calls
- rejected approvals
- approved Stripe MCP calls
- idempotent completed approvals
- expired approvals
- Stripe REST seed script behavior
Tests mock Nebius, Stripe REST, and Stripe MCP with respx, so the default suite is network-free. If you add an optional manual smoke workflow against a live Stripe Sandbox, keep it opt-in and out of pull requests unless the workflow is explicitly marked sandbox-only.
Ship it
A Dockerfile is included and builds a two-stage image (python:3.12-slim builder plus runtime) that runs uvicorn as a non-root app user, with a container healthcheck against /healthz.
make docker
make run-docker
Inherited Prometheus metrics remain available on /metrics. This recipe adds:
approval_events_total{status}stripe_mcp_requests_total{tool,outcome}stripe_mcp_duration_seconds{tool}
LangSmith traces record the prompt, thread, user, environment, and model when tracing is enabled. Stripe key values and checkout URLs are not logged by the app.
Going further
- Persist approvals in Postgres with row-level ownership by authenticated user.
- Add webhook handling for
checkout.session.completed. - Use Stripe restricted keys per environment and rotate them from CI.
- See the rest of the arc in nebius/nebius-partner-cookbook, including the guardrails cookbook this recipe builds on and the Snowglobe testing cookbook that follows it.