User Subscriptions
Let your users run agents on their own Claude or ChatGPT subscription.
Your users can run the agent on their own Claude or ChatGPT subscription, or their own API key, instead of yours.
Store each user’s logins in a credentials Actor and pass it to pi({ credentials }). The agent checks it after apiKeys and before the environment, as described in API Keys. Your application owns login, storage, and refresh:
listreturns the providers the user has a credential for, without the secrets.readreturns a provider’s credential. A subscription login never includes its refresh token.refreshreturns a refreshed credential. Pi calls it when a subscription token expires within five minutes, so the result must stay valid for longer than that.
import { pi } from "@rivet-dev/pi";
import { type Registry, setup } from "rivetkit";
import { credentials } from "./credentials";
const agent = pi({
model: "anthropic/claude-opus-5-5",
credentials: (c) => {
const client = c.client<Registry<{ credentials: typeof credentials }>>();
return client.credentials.getOrCreate([c.key[0]]);
},
});
export const registry = setup({ use: { credentials, agent } });
import { type Credential, InMemoryCredentialStore } from "@earendil-works/pi-ai";
import { ModelRuntime } from "@earendil-works/pi-coding-agent";
import type { PiProviderCredential } from "@rivet-dev/pi";
import { actor } from "rivetkit";
export const credentials = actor({
state: { saved: {} as Record<string, Credential> },
actions: {
save: (c, provider: string, credential: Credential) => {
c.state.saved[provider] = credential;
},
list: (c) => Object.entries(c.state.saved).map(([providerId, { type }]) => ({ providerId, type })),
read: (c, provider: string) => withoutRefreshToken(c.state.saved[provider]),
refresh: async (c, provider: string) => {
const store = new InMemoryCredentialStore();
await store.modify(provider, async () => c.state.saved[provider]);
const runtime = await ModelRuntime.create({ credentials: store, modelsPath: null });
await runtime.getAuth(provider, { minOAuthValidityMs: 10 * 60_000 });
const refreshed = await store.read(provider);
if (refreshed) c.state.saved[provider] = refreshed;
return withoutRefreshToken(refreshed);
},
},
});
function withoutRefreshToken(credential: Credential | undefined): PiProviderCredential | undefined {
if (credential?.type !== "oauth") return credential;
const { refresh: _refresh, ...rest } = credential;
return rest;
}
The agent Actor never receives a refresh token and never writes credentials back. Errors from the credentials Actor reject the model call that needed the credential, so keep secrets out of their messages.
See the complete example, including a script that runs Pi’s headless login in the terminal. Your app needs its own login flow, such as a settings page, that saves the result with the credentials Actor’s save action.
Share credentials across a team
The credentials option picks a credentials Actor from the agent’s key. The example uses the first key part as the user id, so each user has their own logins. To share one set of logins and API keys across a team or organization, start agent keys with the tenant id, such as ["acme", "user-123"]. Each tenant then has one credentials Actor, and an agent only reads its own tenant’s credentials.
Make sure to protect the credentials Actor with authentication, because its actions return provider tokens.