Skip to content

Rose for Agents (r4a)

Prototype (IX-5025)

The r4a-mcp connector is configured for private Cloud Run deployment until buyer authentication is implemented. The only vendor with a working buying funnel is a fake one, Ibanly, which is live at ibanly.app. See What is not built.

What it is

Rose for Websites sells to people visiting a client's site. Rose for Agents sells to AI assistants (Claude, ChatGPT, Cursor, Codex) that are doing a buyer's task: research a category, compare vendors, try a product, sign up, pay, start using it.

r4a is one MCP connector, hosted by Rose, that the buyer installs once. Through it the assistant reaches every Rose customer. A customer provides no tools of its own: r4a exposes the same tools for every vendor and calls the customer's systems on its behalf.

Brand chat (backend/apps/mcp) Rose for Agents (backend/apps/r4a_mcp) WebMCP
Who is working the user talks TO a brand the assistant works on the user's task an AI browser on the client's page
The vendor's agent is the speaker, relayed word for word a source the assistant interprets a source, for that one site
Tools ask_brand_agent, compare_vendors ask_vendor + the buying tools ask_sales_agent
Output ChatGPT card or verbatim text plain data with a provenance note plain text

The two hosted connectors give the model opposite instructions (relay verbatim versus interpret and attribute), which is why r4a is its own app and not a mode of brand chat.

Actors

Actor Runs where Role
Buyer their own machine gives the task, approves paid checkouts, owns the account
Assistant Claude, ChatGPT, Cursor… plans, calls r4a tools, interprets answers
r4a-mcp Rose infrastructure the connector: instructions, tools, request signing
Website agent Rose infrastructure (search API) the vendor's existing Rose agent; answers ask_vendor
Vendor endpoint the customer's infrastructure creates accounts, takes payment, issues credentials

Because r4a is hosted by Rose, anything only the customer's system can do (create an account, issue a credential) is an outbound call from r4a to an endpoint the customer hosts. Evaluating and listing offers need no such call.

The flow

A one-time sign-in, then three phases. Only evaluation needs nothing from the customer's own systems. Labels are shortened to stay legible: every tool call also carries the brand, and vendor paths are the full ones listed in The contract. The assistant may skip the proxied "use" step and call the vendor directly with the credentials it received.

0. Connect and sign in, once

Designed, not built

Today the connector is anonymous. This step is the design; see Buyer identity.

The buyer installs the connector once and signs in once. After that every tool call carries the buyer's token, so r4a knows who is buying without the assistant ever typing an email.

sequenceDiagram autonumber actor B as Buyer participant H as Assistant host participant R as r4a-mcp participant I as Rose sign-in B->>H: add the r4a connector H->>R: first request, no token R-->>H: 401 + where to sign in H->>I: start OAuth (PKCE) I->>B: sign in with work email B-->>I: email code or SSO I-->>H: access token H->>R: requests with the token R->>R: verify token, read buyer identity R-->>H: tools available

The assistant host (Claude, ChatGPT, Cursor) runs this flow by itself: it is the standard MCP authorization flow, triggered by the 401. It is also why sign-in has to cover the whole connector: a server that answers some tools anonymously never sends the 401 that starts the login.

1. Evaluate: the vendor's agent as a source

sequenceDiagram autonumber actor B as Buyer participant A as Assistant participant R as r4a-mcp participant W as Website agent B->>A: task A->>R: ask_vendor(vendor, question) R->>R: resolve vendor R->>W: query W-->>R: answer, sources R-->>A: data + provenance A->>A: interpret, compare A->>R: get_offers(brand) R-->>A: offers, trial, FAQs

2. Try and sign up: r4a calls the customer

sequenceDiagram autonumber participant A as Assistant participant R as r4a-mcp participant V as Vendor endpoint Note over R,V: every request is signed by r4a A->>R: try_product(brand, input) R->>V: POST /trial V-->>R: sample output R-->>A: mode "trial" + output A->>A: judge the real output A->>R: provision_account(brand, email) R->>V: POST /accounts V-->>R: account_id R-->>A: provisioned

3. Pay, get access, use

sequenceDiagram autonumber actor B as Buyer participant A as Assistant participant R as r4a-mcp participant V as Vendor endpoint A->>R: checkout(offer) R->>V: POST …/checkout V-->>R: 402 + amount R-->>A: payment_required A->>B: approve 18.75 EUR? B-->>A: yes A->>R: checkout(offer, token) R->>V: POST …/checkout V-->>R: active + invoice R-->>A: active A->>R: get_access(account) R->>V: GET …/access V-->>R: api_key, plan R-->>A: credentials Note over A,V: use is proxied today A->>R: try_product(input, account) R->>V: POST …/use V-->>R: full output R-->>A: live output A-->>B: result + receipt

A customer on the none backend (Rose for Websites today) stops after phase 1: provision_account answers human_signup_required with the customer's sign-up link.

Tools

Every tool is exposed by r4a. The customer column is what a vendor has to supply.

Step Tool Logic supplied by The customer provides Status
Evaluate ask_vendor r4a, through the vendor's Rose website agent nothing (comes with Rose for Websites) real
Offers get_offers r4a, from the vendor spec data, no code built (spec is a file)
Trial try_product customer handler POST /trial built against the fake vendor
Sign up provision_account customer handler POST /accounts built; buyer email not verified
Pay checkout customer handler POST /accounts/{id}/checkout built with fake tokens; no Stripe
Access get_access customer handler GET /accounts/{id}/access built
Use try_product + account_id customer handler, proxied POST /accounts/{id}/use built

ask_vendor returns vendor, domain, answer, sources, suggested_follow_ups, conversation_id and a provenance line. The connector's instructions tell the model to ask one precise question per call, reuse conversation_id per vendor, compare several vendors, answer in its own words, and attribute claims.

Buyer identity

Designed, not built

provision_account currently takes an email argument from the assistant and the vendor trusts it. That is the main gap between this prototype and a real customer.

Why sign-in is required. Without it, anyone who knows the connector URL can create accounts at a customer in someone else's name, make the customer send email to arbitrary addresses, and spend Rose's LLM budget through ask_vendor. With it, every action has an accountable owner, rate limits and spend caps can be per buyer, and the customer receives a lead it can trust.

Existing-account takeover. Returning an existing account id for an unverified email lets a caller retrieve its API key through get_access, use it or change its plan. Until verified ownership is implemented, both Ibanly signup paths reject existing emails with 409, including concurrent requests, without returning an account id or key. This closes email-based recovery; callers who already possess an account id still need to be checked against the signed-in owner in IX-5119.

How much proof. Identity is not one thing; the level required can depend on what is being bought.

Level Proof Good for
1. Verified email a code sent to the address, or a social or SSO login evaluation, trials, free plans
2. Work identity verified email on a company domain (no free-mail), optionally matching the company the buyer claims to act for paid self-serve plans
3. Verified organisation company SSO, or a DNS record proving control of the domain high-value or contractual purchases

A customer states the minimum level it accepts in its spec; r4a refuses the buying tools below it and tells the assistant what the buyer has to do.

How the customer gets it. r4a, not the customer, authenticates the buyer. The contract then carries a buyer object (email, email_verified, level, stable buyer_id) inside the signed request body, so the signature that proves the call comes from r4a also covers who the buyer is. The customer builds no agent authentication of its own.

What changes in the tools. provision_account loses its email parameter: the account owner is the signed-in buyer. The assistant can no longer sign someone else up, and "only use an email the user gave you" stops being an instruction the model has to follow.

First end-to-end vendor: Ibanly. The visitor signs in to Rose through R4A MCP; R4A creates an Ibanly account for that verified visitor, then uses Ibanly's account-required service. There is no second Ibanly login. All MCP tools require authentication, and subsequent checkout, credential access and service use must verify that the account belongs to the signed-in buyer. This is tracked in IX-5119; Ibanly website-agent onboarding is IX-5120. Rose for Websites signup and trial follow separately in IX-5121.

What it builds on. The installed MCP library (mcp 1.28) supports this directly: the connector runs as an OAuth resource server (token_verifier plus AuthSettings with issuer_url and resource_server_url) and a tool reads the buyer's claims with get_access_token(). The selected authorization server is Supabase Auth, which the backoffice already uses for email-code login, so an r4a buyer and a Rose for Websites user share one Rose identity. Provider configuration and host compatibility still need verification during implementation.

Customers and the vendor contract

A customer is a spec in backend/apps/r4a_mcp/r4a_mcp/specs/<slug>.json plus a backend.

Level Backend Customer effort The assistant can
0 none none evaluate, see offers, get a sign-up link
1 http host the contract below also try, sign up, pay, get credentials, use

Planned, not built: a declarative trial (r4a calls the customer's existing public API from a template) and payment through a connected Stripe account. Both would remove customer code from those two steps, leaving only account creation and access, which no one else can do for the customer.

The contract

Paths are relative to the spec's base_url.

Method and path Request Success Other
POST /trial {input} 200 {output, note} 422 bad input
POST /accounts {email} 201 {account_id} for a new account 409 existing email (until buyer ownership is verified), 422
POST /accounts/{id}/checkout {offer_id, payment_token?} 200 {status, offer, amount_eur, invoice} 402 {status, amount_eur}, 404, 422
GET /accounts/{id}/access 200 {api_key, offer, usage} 409 no active plan, 404
POST /accounts/{id}/use {input} 200 {output, usage} 409, 404, 422

r4a maps a 402 to a payment_required tool result, any other non-success to {"error": …} using the vendor's detail, and a transport failure to an error naming the vendor. It keeps no account state: accounts live at the customer. It never returns base_url or secret_env to the assistant.

Request signing

The customer's endpoints are reachable from the internet, so they must only accept r4a.

sequenceDiagram participant R as r4a-mcp participant V as Vendor endpoint R->>R: ts = now, sig = HMAC-SHA256(secret, "ts.METHOD.path." + body) R->>V: request + X-R4A-Timestamp + X-R4A-Signature alt no secret configured V-->>R: 503 (refuse everything) else header missing, bad signature, or older than 300 s V-->>R: 401 else valid V-->>R: handler result end

Method and path are part of the signed message, so a captured body cannot be replayed against another endpoint. The comparison is constant-time. r4a fails closed too: with no secret configured it does not call the vendor at all.

REST API or SDK?

The REST contract is the product. An SDK is optional sugar, and not needed for the prototype. Five endpoints and JSON bodies are within reach of any backend team, in any language. The reference vendor is about 300 lines of TypeScript with no r4a dependency.

The one part that is easy to get wrong is signature verification: it must hash the raw request bytes (not re-serialised JSON), compare in constant time, and enforce the replay window. That is the same situation as Stripe or GitHub webhooks, and the same answer applies: publish the contract, plus a small verification helper per language.

Option Cost to Rose When
REST contract + OpenAPI + this page already there now
Signature helper (about 30 lines, Node and Python) small first real customer
Full SDK (router, types, idempotency, retries) a maintained library per language when several customers ask, generated from the OpenAPI

Run it locally

One-time setup for the vendor: cd backend/apps/r4a_ibanly && just install.

cd backend/apps/r4a_ibanly && just e2e            # boots Worker + connector, buys through both, stops them
cd backend/apps/r4a_mcp && just up                # or keep them running: Ibanly :8787 + r4a-mcp :8766
claude mcp add --transport http r4a http://127.0.0.1:8766/mcp

Expected: just e2e ends with OK: evaluate, try, sign up, pay, use and access all passed. and exits 0; any wrong step exits non-zero. An unsigned curl to :8787/r4a/trial returns 401, and http://127.0.0.1:8787/ is the demo site.

ask_vendor additionally needs IX_API_KEY and ROSE_MCP_DB_DSN in the environment (and ROSE_API_BASE to target staging). The buying tools need neither.

Tests:

cd backend && poetry run pytest apps/r4a_mcp/tests -q     # the connector, against a scripted vendor
cd backend/apps/r4a_ibanly && just check                  # the vendor, inside the Workers runtime

The vendor is a separate codebase on another stack (a TypeScript Cloudflare Worker) that shares nothing with r4a. That a second stack implements the contract from the REST description alone is the evidence behind REST API or SDK?. Three things keep the two sides from drifting:

  • a shared signature test vector, asserted byte for byte in apps/r4a_mcp/tests/test_commerce.py and in apps/r4a_ibanly/test/worker.test.ts;
  • the connector's tests pin what r4a sends and how it maps each vendor status;
  • just e2e runs the real connector against the real Worker.

Point the connector at any deployment of the vendor with R4A_VENDOR_URL_IBANLY.

What is not built

  • Buyer sign-in and identity. The connector is anonymous and provision_account trusts the email it is given. Design in Buyer identity; Supabase Auth implementation and Ibanly account ownership are tracked in IX-5119.
  • Buyer-facing connector rollout. The deployment workflow sends develop to r4a-mcp-test and main to r4a-mcp-production (prerequisites and staff verification in backend/apps/r4a_mcp/README.md). The test MCP selects the isolated Supabase test-branch database secret; production selects the production secret. Schema migration promotion is tracked in IX-5148. Cloud Run stays behind IAM until buyer OAuth is implemented. Ibanly's signing secret is configured separately in IX-5142. The Ibanly vendor is deployed by hand with wrangler deploy and has no CI yet.
  • Per-customer secrets. One shared local secret; real customers need their own, in Secret Manager, with rotation.
  • Outbound-call safety. base_url comes from Rose's own spec files today. Once customers enter it themselves, r4a needs a public-host allow-list (no private addresses), timeouts and response size caps.
  • Abuse controls. No rate limits or spend caps per buyer.
  • Customer self-service. Specs are files; there is no backoffice editor.
  • Events. Funnel stages are log lines (agent_event), not a table or a dashboard.
  • An automatic, no-sign-up trial of Rose itself, and real payment.

Code map

Path What
backend/apps/r4a_mcp/r4a_mcp/server.py connector, instructions, ask_vendor
backend/apps/r4a_mcp/r4a_mcp/commerce.py buying tools, vendor spec model, signed vendor client
backend/apps/r4a_mcp/r4a_mcp/specs/ one spec per customer (rose.json, ibanly.json)
backend/apps/r4a_ibanly/ Ibanly, the demo vendor for ibanly.app: fake company, real IBAN validation. One Cloudflare Worker (TypeScript): public site, the contract, Durable Object accounts. Reference implementation of the contract. Live at ibanly.app
docs/plans/2026-09-16-ix-5025-rose-for-agents.md plan and decisions