Skip to content

Paywalls, plans and usage billing for AI apps

Design paywalls in the console, ship them with one SDK and collect payments through Stripe.

Public beta. Payments run in Stripe Sandbox; Live payments are not open yet.

MUSE paywall template, rendered in English by the SDK at a desktop viewport.
STILL meditation paywall on a phone: a misty lake at sunrise and the headline “A little space. A quieter mind.”

What's included

Everything is configured in the console. Your app reads it through one SDK.

Onboarding funnels that branch on answers

Design the screens once. Each customer follows the path their answers lead to.

Example flow · Simulated run
Flow editorConsole screenshot · Sandbox
The Indiana flow editor showing a conversion funnel: onboarding pages with an answer branch, a traffic-source branch, a paywall A/B test with a classic and a discount paywall, sign-in, checkout and completion.

Words & stories · Organic · A/B → Classic: Direction → Route by answer → Writing path → Your name → Review → Route by traffic source → Paywall A/B test → Classic paywall → Sign in → Checkout → Complete

Visuals & design · From ads → Discount: Direction → Route by answer → Visual path → Your name → Review → Route by traffic source → Discount paywall → Sign in → Checkout → Complete

This funnel uses:Answer branch · Traffic-source branch · A/B test with 2–20 variants · Sign-in · Checkout

49 paywall templates

Rendered by the SDK, in English and Chinese.

Showing 8 of 43 templates

Real SDK renders of 43 of the 49 templates at a 1440 × 900 desktop viewport; the 6 basic starter templates are not pictured. Prices are template examples.

Integrate with four calls

Three run on your server and one in the browser.

npm install ./vendor/groove-grow-current.tgz
Read the docs

The secret key stays on your server. The browser only holds a customer token, valid for 10 minutes by default.

server/groove.tsTypeScript
import { createGrooveServer } from "@tinkerlandai/indiana-sdk/server"; const groove = createGrooveServer({  baseUrl: "https://grow.your-domain.example",  apiKey: process.env.GROOVE_SECRET_KEY!, // gg_sk_…; keep this on your server  scope: {    projectId: "your-app-uuid",    environment: "sandbox",  },}); // verifiedUser comes from your own verified login sessionconst { customer, created } = await groove.getOrCreateCustomer({  externalUserId: verifiedUser.id,  name: verifiedUser.name,  email: verifiedUser.email,});
Response
{  "created": true,  "customer": {    "id": "…",    "externalUserId": "user_1021",    "attributes": { "tags": [] }  }}
app/api/groove-session/route.tsTypeScript
// Your GET /api/groove-session: issue sessions only for signed-in usersconst user = await requireSignedInUser(request); // Return 401 if the user is not signed inconst { customer } = await groove.getOrCreateCustomer({  externalUserId: user.id,});const { customerToken, expiresAt } =  await groove.createCustomerSession(customer.id); return Response.json(  { customerToken, expiresAt },  { headers: { "Cache-Control": "no-store" } },);
Response
{  "customerToken": "gg_ct_…",  "expiresAt": "2026-10-03T08:10:00.000Z"}
app/groove-client.tsTypeScript
import { createFunnelRunController, showFunnelRun } from "@tinkerlandai/indiana-sdk"; // Your backend retains the original start request and run credential.export async function openFunnel(container: HTMLElement) {const response = await fetch("/api/groove-funnel", {  method: "POST", credentials: "same-origin", cache: "no-store",});if (!response.ok) throw new Error("Funnel bootstrap failed");const { runToken } = await response.json();const client = createFunnelRunController({  baseUrl: "https://grow.your-domain.example",  scope: { projectId: "your-app-uuid", environment: "sandbox" },  runToken,});const handle = await showFunnelRun(container, {  client,  onAuthenticate: async () => obtainCurrentUserCustomerToken(),  onCheckout: ({ url }) => window.location.assign(url),  onOpenUrl: ({ url }) => window.location.assign(url),  onError: reportError,});// Keep the host's original bootstrap across navigation and payment returns.// Destroy removes UI; close is an explicit server command.return { handle, client, destroy() { handle.destroy(); client.reset(); } };}// On route leave: flow.destroy(). On explicit dismissal: await flow.handle.close().
Response
{  "run": {    "status": "running",    "nodeType": "screen",    "revision": 1  }}
server/generate.tsTypeScript
// After a job finishes, spend credits on your serverconst { receipt: spend } = await groove.mutateBalance({  customerId: customer.id,  command: {    operation: "consume", entitlementId, quantity: "1", properties: {},    idempotencyKey: `job:${job.id}`, // Retrying the same job never double-charges  },}); if (!spend.accepted) {  // Insufficient balance: show the upgrade paywall here}
Response
{  "accepted": true,  "id": "…",  "reason": null}

Pricing

  • Starter

    $0

    up to $2,500 in monthly revenue

    Get started
    • All 49 paywall templates
    • Monthly and yearly plans
    • Usage credits and credit packs
  • Growth

    Most popular

    0.8%

    of monthly revenue above $2,500

    Get started
    • Targeting and onboarding experiments
    • Fixed prices in 33 currencies
    • Coupons and promotion codes
  • Scale

    Custom

    volume pricing

    Read the docs
    • Several apps per organization
    • Dedicated onboarding
    • Priority support

Placeholder pricing for layout only, not an offer. During the public beta, payments run only in Stripe Sandbox and nothing is charged.

Questions

Everything else is in the docs.

How does Indiana relate to Stripe?

Stripe collects the money. Indiana owns the plans, paywalls, targeting and entitlements, and reads subscription state from signed Stripe events.

Can I take real money today?

Not yet. During the public beta, payments run only in Stripe Sandbox and nothing is charged. Live payments open after the beta.

Can customers switch between monthly and yearly?

Yes. Monthly to yearly is an upgrade: it applies now and Stripe charges the prorated difference. A lower price per month is a downgrade and applies at the period end by default.

Do I need a new release to change a paywall?

No. Paywalls are fetched when they are shown, so the next display after you publish uses the new version.

Do I put an API key in the browser?

No, and you can't. The browser SDK accepts customer tokens only and throws if given a key starting with gg_sk_.

Will changing a plan affect existing customers?

No. Each publish creates a new version and existing customers keep theirs. A new default free version only applies to customers identified afterwards.

Ship your first paywall in Sandbox

Create an account, pick a template and publish it to your test app.

During the public beta, payments run only in Stripe Sandbox and nothing is charged.

Indiana · Paywalls, plans and usage billing for AI apps