Skip to content

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 install github:inboundx/rose-vendor-sdk#v0.5.0

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 proofId values and check expiresAt at 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 authenticatedAt is 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 agentChangeKeys names, with applyAgentChange. 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 /trial and the anonymous trial described above.
  • auth-provider: set acceptedIssuers to the identity providers you trust, each as { issuer, jwks } with its public keys (Rose ID is one: reuse the rose values). Rose posts the buyer's identity token to /oauth2/token and 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 and trialLimit calls, and issueKey receives both limits: enforce them. The token needs a verified email and your origin as audience. The SDK refuses to start when auth-provider has no acceptedIssuers, 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 issueKey returns whatever token your MCP accepts as Authorization: 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 as https://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:

  1. A signed trial request reaches the intended product action; unsigned requests fail.
  2. Creating an account twice returns the same account for the same buyer and environment.
  3. Another buyer or environment cannot use that account.
  4. A claim keeps the existing trial account and usage, and replaces its credentials.
  5. Replayed claims fail, revocation stops access and an older authentication cannot restore it.
  6. 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.