Build on Scenelet
Embed Scenelet into your own browser plugin, desktop app, or web tool. Your users sign in with their official Scenelet account, and every paid action is metered against their credit balance — the platform handles login, billing, payouts, and remote control, so you only build the feature.
Core concepts
Two identifiers and one token wire your app to the platform. They do different jobs — keep them straight.
- app_key
- Identifies one asset (your plugin/skill) for billing. Issued when you publish the asset; this is where its price lives. Sent on every execute call.
- client_id
- Identifies your OAuth app for the login flow. Created in the Developer apps dashboard. Public clients use PKCE only; confidential (server) clients also get a one-time secret.
- Scenelet token
- The per-user Bearer access token minted after login (valid 90 days by default). The SDK stores it for you; the gateway resolves it to the user whose credits get charged.
1. Get your credentials
You need two things before writing code.
Publish an asset → app_key
Submit your plugin/skill and set its pricing model (free, pay-per-use, subscription, or lifetime). On approval you get an app_key. The price you set here is the only price the gateway will ever charge.
Register an OAuth client → client_id
In the Developer apps dashboard, create a client and add your exact redirect URIs (web callback, a 127.0.0.1 loopback for desktop, or your extension's chromiumapp.org URL). Choose public (PKCE) for plugins and desktop apps, or confidential for a server that can keep a secret.
2. Install the SDK
Use the npm package in a bundler, or load the UMD build via a script tag for a plain extension or page — it exposes window.Scenelet.
# npm (bundlers)
npm install @scenelet/sdk
# CDN (plain <script>, exposes window.Scenelet)
<script src="https://api.scenelet.com/sdk/v1/scenelet-sdk.umd.js"></script>import { Scenelet } from '@scenelet/sdk';
const sc = new Scenelet({
appKey: 'app_xxxxxxx', // the asset being billed
clientId: 'cli_xxxxxxx', // your OAuth app (login flow)
});3. Sign the user in
All three flows are Authorization Code + PKCE and end by minting and storing the user's token. Pick the one that matches your runtime.
Web page — popup
loginViaPopup opens the official login in a popup and resolves once approved. Your redirect page (on your own origin) just calls completePopupCallback().
// 1) trigger login (e.g. on a button click)
await sc.loginViaPopup({ redirectUri: 'https://yourapp.com/oauth/callback' });
// 2) on https://yourapp.com/oauth/callback :
Scenelet.completePopupCallback('https://yourapp.com');Browser extension — chrome.identity
loginViaChromeIdentity drives the flow through chrome.identity. Register chrome.identity.getRedirectURL() as a redirect URI on your client.
// background / service worker / popup script
await sc.loginViaChromeIdentity();
// redirect URI to register: chrome.identity.getRedirectURL()Desktop / server — manual
Build the authorize URL, open the system browser, capture the ?code on your loopback redirect, then exchange it. Confidential clients also pass their secret to exchangeCode().
const { url, codeVerifier, state } = await sc.buildAuthorizeUrl({
redirectUri: 'http://127.0.0.1:8976/cb',
});
// open `url` in the system browser; capture ?code & ?state on your loopback
const token = await sc.exchangeCode({
code, codeVerifier,
redirectUri: 'http://127.0.0.1:8976/cb',
clientSecret: 'csk_xxxxxxx', // confidential clients only
});4. Gate every paid action
Call gate() right before a metered action. It phones the gateway and, on denial, shows the official topup / login modal for you — you render no billing UI. Use checkAuth() instead if you want to handle the result yourself.
if (!(await sc.gate())) return; // SDK shows topup / login modal on denial
runMyPaidFeature();
// …or handle the result yourself:
const r = await sc.checkAuth();
if (!r.ok) showMyOwnUI(r.reason, r.message);The execute API
Prefer the SDK, but the gate is a single HTTP call you can make from anywhere. POST the asset id as app_id with the user's Bearer token. feature_cost is advisory only — the gateway always charges the asset's server-configured price, so a client can never under-report to pay less.
POST https://api.scenelet.com/v1/sdk/execute
Authorization: Bearer <scenelet-token>
Content-Type: application/json
{ "app_id": "app_xxxxxxx", "tier": "standard" }{
"ok": true,
"status": "authorized",
"userId": "u_abc",
"remaining_credits": 95,
"runId": "run_xyz",
"actualTier": "standard"
}{
"ok": false,
"status": "forbidden",
"reason": "insufficient_funds",
"message": "需要 50 点,当前 32 点",
"topupUrl": "https://scenelet.com/topup"
}ok:true means the credit was charged and the run is cleared; keep the runId. A 401 means the token is invalid or expired; 403 means a daily quota was hit; 429 means a per-minute flood limit — honor Retry-After.
Denial reasons
Every non-ok response carries a stable reason string. Handle these to give users the right next step — the SDK's modal already maps them.
| reason | What it means |
|---|---|
| insufficient_funds | Not enough credits for this run — send the user to topup. |
| not_purchased | A subscription/lifetime asset the user hasn't bought yet. |
| subscription_expired | Their subscription lapsed — prompt to renew. |
| plugin_disabled | The platform remotely disabled this asset (kill-switch). |
| bad_app_key | Unknown, unpublished, or malformed app_key. |
| no_user_token | No token, or it's invalid/expired — prompt login. |
| rate_limited | Per-minute flood limit hit (429) — retry after the header. |
| quota_exceeded | Daily tier quota exhausted (403). |
| network_error | The SDK couldn't reach the gateway. |
How billing works
You set a price; the platform collects credits from the user, splits the revenue, and pays you out. There is nothing to reconcile on your side.
- Free — always allowed, no charge.
- Pay-per-use — debits priceCredits from the user on each successful run.
- Subscription — one upfront purchase, then unlimited runs until it expires.
- Lifetime — one upfront purchase, unlimited runs forever.
Control & safety
The platform keeps levers over every integration — useful for you (abuse protection) and enforceable by us (policy).
- Remote kill-switch — an asset can be disabled instantly; execute then returns plugin_disabled and the SDK shows a notice. No redeploy on your side.
- Tiered quotas — anonymous < member < paid daily caps (admin-tunable). Anonymous users hitting the cap get trigger_auth_modal so you can prompt signup.
- Flood protection — a per-minute limit returns 429 with Retry-After before the daily quota is even checked.
- Token expiry — tokens last 90 days by default and resolve fresh on every call, so revoked or expired credentials stop working within seconds.
- Refund-safe — the charge commits when execute returns ok. If the platform's execution backend fails downstream, it reverses the debit via an internal endpoint; you don't call it yourself.
Security notes
- PKCE is S256-only — the plain method is rejected.
- redirect_uri must exactly match one registered on the client — no wildcards.
- Authorization codes are single-use and expire in 60 seconds.
- Confidential client secrets are shown once and stored hashed — rotate by creating a new client.