Install and set up the vendor SDK¶
Use the Rose vendor SDK to connect your product's server to Rose for Agents. It handles signed request verification and routing, provides verification helpers for Handover, and can run agent access (auth.md trials and claims) on your accounts. Your product keeps its existing account storage, permissions and business logic.
Contact your Rose account manager to arrange the integration and receive your server configuration. The SDK is open source (Apache-2.0): github.com/inboundx/rose-vendor-sdk.
1. Install¶
In your server project, using Node.js 22 or later and ES modules:
npm builds the release on install. The package includes JavaScript and TypeScript declarations; it is installed from GitHub, not from the npm registry.
Install the SDK on your server. There is no secret to store: Rose signs every request with its own key and the SDK checks it against Rose's published public keys.
2. Connect your product operations¶
Implement a VendorHandlers object using your existing product services:
| Handler | Connect it to |
|---|---|
trial |
A limited product action that returns a useful sample result. |
account |
Account creation or lookup for the verified buyer. |
lookup |
For Handover, the buyer's existing account, including a claimed trial. |
authorizeAccount |
A check that the account belongs to this buyer in this environment. |
checkout |
Your offer activation and payment flow. |
access |
The credentials and access your product grants. |
use |
A permitted product action and its usage counter. |
Then create the Rose request handler:
import { createVendorHandler, type VendorHandlers } from '@userose/vendor-sdk';
export function connectProduct(origin: string, handlers: VendorHandlers) {
return createVendorHandler({ origin, handlers, basePath: '/r4a' });
}
Set origin to your public API origin, such as https://api.example.com. Rose signs each
request for that host, so a request Rose signed for another company is refused by your
handler. The SDK tells you whether a request comes from Rose's test or production
environment; keep their accounts apart.
Mount the returned handler at /r4a/* in a server route that accepts a standard Web
Request and returns a Response. Give Rose its base URL, such as
https://api.example.com/r4a. If adapting another server framework, preserve the exact
request body and public URL; avoid trusting arbitrary forwarded host headers.
Account lookup and permissions must use environment + buyerId as the owner. Do not
merge users by email. Make account creation and payment operations safe to retry. The
SDK calls authorizeAccount before checkout, access and use; recheck authorization in
your product transaction if ownership or access can change concurrently.
For Handover, also implement lookup(buyer) to return {account_id} for an owned account
or null if none exists. This supplies the signed GET /r4a/account endpoint so Rose can
reuse an account before creating a trial. A missing lookup handler returns 503 and stops
registration rather than creating a duplicate account.
For expected product errors, throw VendorError with an appropriate status, for example
new VendorError(409, 'No active plan'). An unpaid checkout can throw
new VendorError(402, {status: 'payment_required', amount_eur: 10}).
3. Connect Handover¶
For Handover, Rose provides a trust configuration with the issuer, vendor audience, client identifier, environment, accepted identity sources and public signing keys. In production, these values are public:
| Setting | Value |
|---|---|
Issuer of Rose's claims (brokerIssuer) |
https://mcp.userose.ai/identity |
Rose's public signing keys (brokerJwks) |
https://mcp.userose.ai/identity/.well-known/jwks.json |
Client identifier (brokerClient) |
rose-id |
| Environment | production |
| Accepted sign-in sources | google |
Page where the customer approves the claim (consentUrl) |
https://id.userose.ai/agent/authorize |
Origin allowed to post the claim to you (CORS, browserOrigin) |
https://id.userose.ai |
Your audience is your own public origin, which Rose registers together with your vendor identifier and your claim, registration and event URLs. You also keep your own ES256 signing key: the private key stays a secret on your server, and its public key goes in your configuration. Pass the trust configuration to the verifier:
import { createHandoverVerifier } from '@userose/vendor-sdk';
const handover = createHandoverVerifier(roseTrustConfiguration);
const claim = await handover.verifyClaim(identityAssertion, {
registrationId: trial.id,
userCode: trial.userCode,
});
await yourDatabase.claimTrialAtomically(claim);
const event = await handover.verifyRevocation(signedEvent);
await yourDatabase.revokeBuyerAtomically(event);
Load trial from your database. The expected account and code must come from your
stored trial, rather than unverified assertion fields or values supplied by a requester.
The SDK verifies proofs; your account transactions apply them. When claiming a trial:
- Reject reused
proofIdvalues and checkexpiresAtat commit time. - Check the stored trial's code, expiry and owner, and resolve existing-account conflicts.
- Preserve the account and usage, replace trial credentials and let the original assistant resume with the approved access.
- Check that
authenticatedAtis later than the buyer's stored revocation cutoff in the same transaction as the claim.
When revoking access, process eventId idempotently, advance the buyer's cutoff to at
least issuedAt, and invalidate all delegated credentials and resume access before
acknowledging the event. Older events must never lower the cutoff. Times use Unix seconds.
Rose will work through the trust configuration and rollout with your team. Replace the verifier with the updated trusted key configuration when Rose rotates its signing keys.
Let the SDK run agent access¶
Instead of building trial creation, the claim page, credential delivery, resume and expiry
yourself, let the SDK run them: createAgentHandler serves auth.md and its endpoints
(/auth.md, /.well-known/*, /agent/*, /oauth2/*, /claim). Its configuration holds
Rose's trust configuration, your own ES256 signing key, how auth.md describes your product,
and the calls a trial key allows. You supply two things:
- Accounts: create an account, issue and revoke a key with your own API-key code (a trial key carries an expiry and a call limit: enforce them), record the person who claimed an account, and find the account a key belongs to.
- A store: read a record, and apply one change in one transaction over the records
agentChangeKeysnames, withapplyAgentChange. Every claim and revocation check happens inside that transaction, so concurrent claims cannot both win.
import { agentChangeKeys, applyAgentChange, createAgentHandler, type AgentStore } from '@userose/vendor-sdk';
const store: AgentStore = {
read: (key) => yourRecords.get(key),
apply: (change) => yourRecords.transaction(async (tx) => {
const records = Object.fromEntries(await Promise.all(
agentChangeKeys(change).map(async (key) => [key, await tx.getForUpdate(key)] as const)));
const { writes, outcome } = applyAgentChange(change, records, Date.now());
for (const [key, value] of Object.entries(writes)) await (value === null ? tx.delete(key) : tx.put(key, value));
return outcome;
}),
};
const agentAccess = createAgentHandler({ ...roseAgentConfiguration, accounts: yourAccounts, store });
// Answer agent requests first; null means the request is not one of them, so your API answers,
// and its 401 gains the challenge that leads an agent with no key to auth.md.
const answer = await agentAccess.handle(incomingRequest)
?? await agentAccess.challenge(await yourApi(incomingRequest));
Wrap your API's responses with challenge. An agent that calls your API without a key and gets
a bare 401 concludes that only a person can sign up; the challenge
(WWW-Authenticate: Bearer resource_metadata=…, RFC 9728) is where auth.md discovery starts,
and a JSON body also gains a one-line pointer to /auth.md. Link /auth.md from your API docs
and llms.txt as well: in Rose's tests, that link is what agents followed. Wrap only the API an
agent calls, not the 401 of a page a person signs in to.
agentAccess.contract gives your /r4a handlers the same records (lookup, provision,
owns, check and access), so an account claimed from a trial is the buyer's account
through Rose too.
Optional anonymous trial protection¶
Set trialProtection: "rose" in createAgentHandler to use the policy configured by
Rose staff under Config Studio → Rose for Agents → Agent access → Anonymous agents.
The broker policy endpoint must be available before enabling this option; an unavailable
policy blocks anonymous registration. Verified Rose ID requests are exempt.
The agent solves the computational challenge itself using the solver linked from your
/auth.md, then retries registration; no person or CAPTCHA is needed. Omitting the option
keeps protection off. Vendors can also choose "off", "low", "medium" or "high"
directly. Keep trial quotas and rate limits: proof of work adds cost to repeated trials,
but does not guarantee one trial per person.
Choose how Rose trials your product¶
Set mode in createAgentHandler. The SDK publishes it in your OAuth metadata, and Rose reads
it from there.
contract(default): the signed/trialand the anonymous trial described above.auth-provider: setacceptedIssuersto the identity providers you trust, each as{ issuer, jwks }with its public keys (Rose ID is one: reuse therosevalues). Rose posts the buyer's identity token to/oauth2/tokenand gets a trial key back, then the buyer's assistant uses your own MCP server or API. One person gets one account and one trial of an hour andtrialLimitcalls, andissueKeyreceives both limits: enforce them. The token needs a verified email and your origin as audience. The SDK refuses to start whenauth-providerhas noacceptedIssuers, or the reverse.catalogue: Rose runs the trial with its own key. Not available yet; the SDK refuses it.
When your product is an MCP server¶
An assistant can use your MCP server before anyone signs in, and keep the account once the
customer claims it. Rose's get_access gives the assistant your MCP's URL and a trial token;
the assistant calls your MCP directly.
- Trial token: your
issueKeyreturns whatever token your MCP accepts asAuthorization: Bearer(an API key, or an access token from your authorization server), with the trial's expiry and call limit. The claim replaces it with a full token on the same account. product.mcpUrl: your MCP's URL, such ashttps://api.example.com/mcp. auth.md and the token response (mcp_url) point the assistant to it.authorizationServer: if your own OAuth server (the sign-in MCP clients use for people) runs on the same origin as the agent endpoints, pass its metadata and token handler:
const agentAccess = createAgentHandler({
...roseAgentConfiguration,
product: { ...roseAgentConfiguration.product, mcpUrl: 'https://api.example.com/mcp' },
authorizationServer: {
metadata: yourOAuthMetadata, // authorization_endpoint, registration_endpoint, grant types…
token: (fields, request) => yourTokenEndpoint(fields, request), // authorization_code, refresh_token…
},
accounts: yourAccounts,
store,
});
The SDK merges your metadata into /.well-known/oauth-authorization-server and hands
/oauth2/token every grant it does not run itself. Your other routes (/oauth2/authorize,
/oauth2/register, your MCP's /.well-known/oauth-protected-resource/<path>) stay yours.
Keep your MCP's own 401 challenge (resource_metadata pointing to that MCP's metadata), so
MCP clients still find your sign-in.
Let the customer sign in to the claimed account¶
After the claim, the customer will also sign in to your product directly: your dashboard, or
connecting your MCP server with its normal sign-in. They should land on the account they
claimed, not a new empty one. The claim verified their email: the SDK passes it to your
claimed(accountId, {email}). Record it, and when someone signs in to your product with that
verified email, open that account.
Your sign-in must tell your server the person's email and that it is verified. Where that comes from depends on your authentication provider:
| Provider | Where your server reads the verified email |
|---|---|
| Clerk | The session token has no email by default. In the Clerk dashboard, Configure → Sessions → Customize session token, add {"email": "{{user.primary_email_address}}"}, and keep email verification required at sign-up. |
| Auth0 | The ID token's email and email_verified claims (email scope). For an access token, add them as custom claims in a post-login Action. |
| Supabase Auth | The access token's email claim; check the user's email_confirmed_at, or require email confirmation in the Auth settings. |
| Firebase Authentication | The ID token's email and email_verified claims. |
| Other OpenID Connect providers (Okta, Keycloak, Google…) | The email and email_verified claims, with the email scope. |
| Your own email sign-in (magic link, code) | The address the link or code was sent to: it is verified once used. |
Link only a verified email: if your sign-in accepts an unverified one, anyone could reach an account by signing up with the customer's address. If the customer claims several trials with one email, decide which account their sign-in opens (for example, the last one claimed).
4. Verify with Rose¶
Before rollout, test your configured integration together:
- A signed trial request reaches the intended product action; unsigned requests fail.
- Creating an account twice returns the same account for the same buyer and environment.
- Another buyer or environment cannot use that account.
- A claim keeps the existing trial account and usage, and replaces its credentials.
- Replayed claims fail, revocation stops access and an older authentication cannot restore it.
- For an MCP server: the trial token works on your MCP, the replacement token works after the claim, and the customer's own sign-in reaches the claimed account.
Return to Connect your product or the Rose for Agents overview.