@useoutlet/sdk

Connect your users' AI accounts to your app. Capped, revocable App keys, never raw credentials. Outlet is never in the data path.
2,355
Install
npm install @useoutlet/sdk
Latest Version:0.7.0
Vue:3
License:MIT
Last Updated:Oct 9, 2026
Links: GitHub  ·  npm
Maintainer: parzcode

@useoutlet/sdk

Let your users plug in the AI they already pay for.

Outlet connects a user's existing AI account (Anthropic, OpenAI, Google, or any OpenAI-compatible provider) to your app through capped, revocable App keys, one per app. Never their raw credentials. Your app calls the provider directly with the official SDK. Outlet is never in the data path.

Status: direct mode works today (validated bring-your-own-key, no server). Vault mode (capped, revocable App keys) is open. Register your app at useoutlet.dev/register. The same session in both modes. Protocol docs at useoutlet.dev. Feedback welcome.

Building with an AI? Paste this into your AI.

Your AI adds the Connect your AI button for you. The same button can offer Direct, Vault or both. Register your app at useoutlet.dev/register for the app ID.

Add Outlet to this app so users can connect their own AI account.
Read https://useoutlet.dev/llms-full.txt first.
Install: npm install @useoutlet/sdk
App ID: <from useoutlet.dev/register>
Return address: <the https or private-scheme return address>
Local testing: http://localhost/outlet/return, any port
Button: import { mountConnectButton } from "@useoutlet/sdk/ui".
Mount on an empty div with mode: "both", providers, appId and redirectUri.
React or Vue: use useConnectButton from "@useoutlet/sdk/react" or "@useoutlet/sdk/vue".
Vault return: pass Outlet.handleRedirect() as session only when the return address has code and state.
Receive the Direct or Vault session through onSession.
No app secret in the app.
Vault provider: choose openai, anthropic, fal or openrouter. Use a separate Vault connection request for each provider.
Place the Connect your AI button where users connect their AI account.
After connect: call the provider with its official SDK using session.keys.<provider>.

Give your AI tool the docs

Claude Code: install the Outlet plugin. It adds the /outlet:add-outlet skill.

claude plugin marketplace add PARZ1V3L/outlet
claude plugin install outlet@useoutlet

Any tool with MCP: run the docs server. It serves the current docs and the setup prompt.

claude mcp add outlet -- npx -y @useoutlet/sdk mcp

.cursor/mcp.json:

{
  "mcpServers": {
    "outlet": { "command": "npx", "args": ["-y", "@useoutlet/sdk", "mcp"] }
  }
}

Cursor: copy the rule file into .cursor/rules.

cp ai-tools/cursor/outlet.mdc .cursor/rules/outlet.mdc

ai-tools/

Why

  • Users already pay for AI. They shouldn't pay again inside every app.
  • Developers shouldn't store customer API keys (breach target, compliance).
  • Raw keys are all-or-nothing. Outlet keys are per-app, capped, revocable.

Install

npm install @useoutlet/sdk

No build step? Import the button from a CDN:

import { mountConnectButton } from "https://esm.sh/@useoutlet/sdk/ui";

Quickstart

npm install @useoutlet/sdk

Add the Connect your AI button to a page. The same button can offer Direct, Vault or both. This example offers both for OpenAI. Vault needs a registered app ID and return address. Register your app to get them. Serve this page at your registered return address too.

<div id="connect"></div>
<script type="module">
  import Outlet from "@useoutlet/sdk";
  import { mountConnectButton } from "@useoutlet/sdk/ui";
  const params = new URLSearchParams(location.search);
  const returning = location.pathname === "/outlet/return"
    && params.has("code") && params.has("state");
  let session;
  const button = mountConnectButton(document.getElementById("connect"), {
    mode: "both",
    providers: ["openai"],
    appId: "app_yourapp", // Your registered Vault app ID.
    redirectUri: location.origin + "/outlet/return",
    requestedCapUsd: 10, // Your proposed monthly Vault cap in USD.
    session: returning ? Outlet.handleRedirect() : undefined,
    onSession(connected) {
      session = connected;
      // Enable your app's AI action here.
    },
  });
</script>

Bundle the page with your build. This example uses Vite.

npx vite build

onSession receives the session after a connection completes. session.keys.openai holds the Direct API key or Vault App key. Both modes return the same session shape. Then call the provider like you already do, with its official SDK.

When a connection ends

A connection can end while your app is running: the user's Vault connection is paused at its cap, revoked or disconnected, its refresh token is gone, or the provider refuses a Direct API key. The SDK turns each into one typed error, ConnectionEndedError, with reason (capped, revoked, expired or refused) and grantId. status() and refresh() throw it. To catch the provider's own refusal too, hand the provider's SDK the wrapped fetch. A provider answer of 401, 402, 403 or 429 becomes one status() check. An open connection gets the provider's answer back untouched.

const ai = new OpenAI({
  apiKey: session.keys.openai,
  fetch: Outlet.wrapFetch({ session: () => session }),
});

The Connect your AI button hears about the end and shows one screen with the one thing the user can do: Raise the Vault cap, Connect again, or Paste a new Direct API key. When they are done, onSession receives the new session, the same way it received the first. Your app writes nothing else. Mount the button with the session you hold, so it knows the grant.

Phone apps: the same ends come back as ConnectionEndedError from refresh() and status(). Run the grant again in the phone's browser sheet for a revoked or expired connection. For a capped one, open https://useoutlet.dev/account/ in the sheet and call refresh() when the sheet closes.

Usage today (direct mode)

Your user pastes their own API key; the SDK validates it locally (catches provider mix-ups, refuses admin keys) and hands back a session. No network, nothing sent to Outlet. A local format check.

import Outlet from "@useoutlet/sdk";

const session = await Outlet.direct({
  keys: { openai: userPastedKey },
});

import OpenAI from "openai";
const ai = new OpenAI({ apiKey: session.keys.openai });

Works with any OpenAI-compatible provider. Pass the key under that provider and point the OpenAI SDK at its base URL:

const session = await Outlet.direct({ keys: { groq: userPastedKey } });

const ai = new OpenAI({
  apiKey: session.keys.groq,
  baseURL: "https://api.groq.com/openai/v1",
});

OpenAI, Anthropic, and Google get strict key-format checks (mix-ups caught, admin keys refused); other providers are accepted with the same admin-key safety check, since their key formats vary.

ChatGPT plan

A way in beside Direct and Vault. The user signs in at OpenAI and your app spends their ChatGPT plan. It is free in Outlet, like Direct. No Outlet server is touched, and no token goes to Outlet.

OpenAI allows ChatGPT plan use in open-source apps and in personal projects that run on the user's own machine. A paid or hosted app needs OpenAI's approval first.

The entry is @useoutlet/sdk/plan. It runs in Node, on the user's machine. It listens on 127.0.0.1 for OpenAI's return and keeps the sign-in in a file. It cannot run in a browser page.

import OpenAI from "openai";
import Outlet from "@useoutlet/sdk";
import { connectPlan, restorePlan } from "@useoutlet/sdk/plan";

const app = { provider: "openai", appName: "Your App" } as const;
let session = (await restorePlan(app)) ?? (await connectPlan(app));

const ai = new OpenAI({
  apiKey: session.keys.openai,
  fetch: Outlet.wrapFetch({ session: () => session }),
  maxRetries: 0,
});

The five calls:

  • connectPlan({ provider: "openai", appName }) opens the user's browser for OpenAI's sign-in and returns a session. The first time, the user approves your app by name. Label the action Continue with ChatGPT.
  • restorePlan({ provider, appName }) returns the saved session, or null. It opens no browser. A token near its end is refreshed first.
  • refreshPlan(session) returns a session with a new token.
  • forgetPlan({ provider, appName }) ends the sign-in at OpenAI and deletes the local record. The user disconnects the app itself in ChatGPT settings.
  • planModels(session) returns the models the account may use.

The session is the same OutletSession:

  • mode is "plan".
  • keys.openai holds a token that lasts an hour. It goes where an API key goes.
  • expiresAt is that token's expiry.
  • capUsd is Infinity. Outlet sets no cap here. The cap is the user's own setting at OpenAI.
  • The refresh token is never on the session. It lives in the store.

The store is a file only the user can read, at ~/.config/<your app>/outlet-plan.json. Pass store to keep the sign-in somewhere else, a keychain for one. This version keeps one account per app.

What ends it is the same typed error, ConnectionEndedError:

  • capped: the plan or your app reached its limit at OpenAI. Show Manage usage, linked to https://chatgpt.com/settings/usage.
  • revoked: the user disconnected your app.
  • expired: the sign-in can no longer be refreshed.
  • refused: OpenAI does not allow plan use on this account.

The wrapped fetch throws capped and refused. OpenAI's SDK reports an error from its fetch as a connection error. The ConnectionEndedError is on its cause. refreshPlan() and restorePlan() throw revoked and expired. When OpenAI answers 401, call refreshPlan().

A limit can also arrive inside a stream. The wrapped fetch never reads a stream. Hand each event to planStreamEnd:

import { planStreamEnd } from "@useoutlet/sdk/plan";

for await (const event of stream) {
  const end = planStreamEnd(event, session);
  if (end) throw end;
}

OpenAI never moves the user to other billing by itself. When the plan ends, offer a Direct API key or Vault. Outlet.direct() returns the same session shape. examples/plan/ does exactly that.

OpenAI's limits for plan use: the Responses API only, with store: false and stream: true. Some request fields and tools are not supported. OpenAI's page has the list. While requests run on the plan, show Using ChatGPT plan near where the user types.

React and Vue

The button comes as a React hook and a Vue composable.

import { useConnectButton } from "@useoutlet/sdk/react";
import { useConnectButton } from "@useoutlet/sdk/vue";

Both take the same options as mountConnectButton and clean up on unmount.

Pass directKeyStorage: "browser" or "server" and the Direct paste screen says where your app keeps the person's Direct API key.

import { useConnectButton } from "@useoutlet/sdk/react";

function Connect() {
  const { ref } = useConnectButton({
    mode: "both",
    providers: ["openai"],
    appId: "app_yourapp",
    redirectUri: "https://yourapp.com/outlet/return",
    onSession: (session) => { ai = new OpenAI({ apiKey: session.keys.openai }); },
  });
  return <div ref={ref} />;
}
// In Next.js, mount the button in a client component.
<script setup lang="ts">
import { useConnectButton } from "@useoutlet/sdk/vue";

const { target } = useConnectButton({
  mode: "both",
  providers: ["openai"],
  appId: "app_yourapp",
  redirectUri: "https://yourapp.com/outlet/return",
  onSession: (session) => { ai = new OpenAI({ apiKey: session.keys.openai }); },
});
</script>

<template>
  <div ref="target"></div>
</template>

Vault mode (the same session)

Same session shape. The pasted key becomes an App key: provisioned inside the user's own account for your app alone, capped, and revocable.

// NOTE: register your app at useoutlet.dev/register to get your app_id.
//
// 1. user clicks [ Connect your AI ], which opens the Outlet grant screen
const session = await Outlet.connect({
  appId: "app_yourapp",
  providers: ["openai"],
  requestedCapUsd: 10,
});

// 2. same as before
const ai = new OpenAI({ apiKey: session.keys.openai });

// later: re-fetch keys (session.grantId is the thing you persist)
const fresh = await Outlet.refresh(session.grantId);

// check spend / status
const info = await Outlet.status(session.grantId);
console.log(`${info.spendUsd} of ${info.capUsd} used`);

No backend? Public clients use PKCE

connect() suits apps with a server (it can hold an appSecret). A pure frontend (SPA, phone app, CLI) registers as a public client instead: no secret, just a registered redirectUri. Grant creation is secured by PKCE (protocol docs at useoutlet.dev), so the flow spans the redirect in two calls:

// on your [ Connect your AI ] button: generates PKCE, redirects to the grant screen
await Outlet.connectRedirect({
  appId: "app_yourapp",
  providers: ["anthropic"],
  redirectUri: "https://yourapp.com/outlet/return",
});

// on the page served at redirectUri: verifies state, exchanges the code
const session = await Outlet.handleRedirect();
const ai = new Anthropic({ apiKey: session.keys.anthropic });

// session.refreshToken (not an app secret) authorizes later calls.
const fresh = await Outlet.refresh(session.grantId, { refreshToken: session.refreshToken });
// fresh.refreshToken replaces the one you sent; the old one now gets 401. Store the new one.

The provider key arrives only on the back-channel exchange, never in the redirect URL. The refreshToken belongs to this one grant.

Outlet registers http://localhost/outlet/return for every app. It matches any port.

Phones

The public-client flow works inside iPhone and Android apps. Open the grant URL in the system browser sheet, take the code back on your registered return address (a private scheme like com.yourapp:/outlet/return, or your website), then call exchangeCode(). React Native has no Web Crypto: pass crypto: { getRandomValues, sha256 } from expo-crypto to createGrant(). Guide: https://useoutlet.dev/docs/dev/mobile

Media apps

Choose the provider that serves the models your app needs. The provider and model determine which images, video or voice your app can make.

Model hosts

fal serves models from other makers for image, video and voice apps. Use an API-scope fal Direct API key for Direct. For Vault, the user adds an ADMIN-scope fal Vault admin key on useoutlet.dev. Your app receives a separate Vault App key.

Replicate serves models from other makers. Use a Replicate API token as a Direct API key. Replicate is Direct only because it does not provide an API for Outlet to create separate Vault App keys.

Model makers

OpenAI’s gpt-image-2 and gpt-image-2.5 image models run through the existing OpenAI Vault connection. Anthropic can provide text alongside media through Direct or Vault. Google’s Gemini API is Direct only. Veo uses a long-running operation; submitting a request does not mean the result is ready.

Other providers can use the generic Direct screen. Your app supplies the provider’s name and official page for getting a Direct API key. Implement and check that provider’s model calls in your app.

mountConnectButton(target, {
  mode: "direct",
  providers: ["fal", "replicate", { id: "runway", name: "Runway", keysUrl: "https://dev.runwayml.com/" }],
  onSession,
});

Vault caps for media

fal does not enforce a spending limit on each Vault App key. Outlet measures the Vault App key’s spending from delayed fal reports. Outlet deletes the Vault App key when reported spending reaches the Vault cap. Spending can exceed the Vault cap, especially when jobs are costly or already running. Revoking Vault access may not stop work the provider has accepted.

Background jobs and workers

Use Vault for work your app must manage after the browser closes. Keep the Vault connection and its refresh credentials in protected server storage, associated with the user, app and provider. Your worker loads that connection to obtain the app’s Vault App key. The app never receives the Vault admin key.

Refresh the Vault connection before access expires. Save the refreshed connection before the next job uses it. Check Vault access before submitting more work.

On a 401 response, stop submitting jobs and check whether the failure came from Outlet or the provider. If the Vault connection has expired, refresh it. If refresh fails or access is no longer active, ask the user to reconnect through Outlet. Do not repeatedly retry a rejected Vault App key or resubmit a generation whose outcome is unknown.

If Vault access is paused, stop new work and show that the connection needs attention on Outlet. If Vault access is revoked, require the user’s approval before reconnecting. If the Vault cap is reached, stop new work and show that the Vault cap has been reached. Do not create another Vault connection to bypass the Vault cap.

For fal, submit long jobs to its queue. Save the request ID with the job so the worker can check that request instead of submitting it again. Use polling or fal webhooks to collect the result. Verify webhook signatures and handle repeated deliveries without processing the result twice.

When Vault access stops, stop local retries and further submissions. Request cancellation of pending provider work where available. Cancellation may fail to stop work already processing, so a job can still finish and be charged after Vault access is stopped.

Direct API keys are never sent to Outlet. Your app holds the key, in the browser or on your own server when the provider accepts calls only from a server, and tells the user which. Work that runs after the browser closes belongs in the Vault.

Several providers

Create a separate connection for each provider your app uses. A text connection can sit alongside connections for video and voice. If the same provider serves several models, those models can use that provider’s connection.

For example, an app can use Anthropic in Vault for text and fal in Vault for video and voice. Each provider’s Vault connection has its own Vault App key, Vault cap and revoke control. Video and voice using the same fal Vault App key share its Vault cap.

Keep each Vault connection associated with its provider when saving, refreshing and using it. Revoking the fal Vault connection does not revoke the Anthropic Vault connection.

You can also offer a Direct connection, such as Replicate, alongside Vault. Outlet never receives a Direct API key. Direct does not get Vault caps or Outlet’s Vault revoke control. The user removes Direct access on the provider’s website.

What your app never sees (vault mode)

  • The user's root API key or account credentials
  • Other apps' keys or spend
  • Anything after revocation. A revoked grant stops working

The user's own key, in your app, by their choice. Validated, never an admin credential. The same API you keep when you upgrade to vault mode.

What Outlet never does

No proxying. No token markup. No model routing. No prompt storage. Free for end users, forever. Direct mode is free. The vault is the paid product for developers. MIT licensed.


Docs: useoutlet.dev · Source · Contact: hello@useoutlet.dev

Related Packages

nuxt-auto-translate

Automatic i18n translations for Nuxt using LLMs (OpenAI, Anthropic, Gemini, Groq...

2,009 2
openmagic

AI-powered coding toolbar for any web application. Select elements, capture cont...

15,193 22
vue-llm

AI/LLM composables for Vue — useChat, useCompletion, streaming, multi-provider....

0 0
@aibind/vue

AI SDK bindings for Vue 3 — reactive composables for streaming, structured outpu...

1,366 19
@aibind/nuxt

AI SDK bindings for Nuxt — composables with defaults, server handlers, and agent...

1,296 19

Version History

Version Released License
0.7.0 MIT
0.6.3 MIT
0.6.2 MIT
0.6.1 MIT
0.6.0 MIT