install

One identity. One client.

@wrldbld/dots is a drop-in SDK that adds Sign in with dots, cross-app credits (pute), verified attestations, and per-user data storage to any app. Configure it with the public API key from your dots app.

Every dots login strengthens one portable identity. Overlapping verified signals appear on /connect so people can connect across the apps you ship — feeding a shared context engine without merging each app's private data.

quick start

  1. 1. Register an app at /dev/clients.
  2. 2. Copy the public DOTS_API_KEY.
  3. 3. Use OIDC/REST now, or install a local SDK tarball.
  4. 4. Add a PKCE /dots/callback route.

install

Add the SDK

The npm package is not published yet. Agents and CI must verify the package exists before installing; use the REST/OIDC API or the local tarball in the meantime.

# npm publishing is pending — verify before installing
npm view @wrldbld/dots
# after publication:
npm i @wrldbld/dots@alpha

local install (until npm publish lands)

# from a local clone of the monorepo (while npm publish is pending)
git clone https://github.com/worldbuild-co/wrldbld
cd wrldbld && bun install && cd packages/dots && bun run build && npm pack
npm i /path/to/wrldbld/packages/dots/wrldbld-dots-0.1.0-alpha.0.tgz

Agent-readable integration rules: /llms.txt.

env

Configure the API key

The API key is your dots app's public OAuth client id. It's safe in client code (it's not a secret). Public/native clients use PKCE — no client secret needed.

NEXT_PUBLIC_DOTS_API_KEY="dots_client_..."  # your dots app's public client id
# optional override (defaults to https://www.dots.id):
# NEXT_PUBLIC_DOTS_ISSUER="https://www.dots.id"

better auth

Use dots with Better Auth

dots is a standard OIDC provider. Better Auth owns your app session; the dots sub remains the portable identity. Register /api/auth/callback/dots as the exact callback URL.

import { betterAuth } from "better-auth";
import { genericOAuth } from "better-auth/plugins";

export const auth = betterAuth({
  plugins: [
    genericOAuth({
      config: [{
        providerId: "dots",
        discoveryUrl:
          "https://www.dots.id/.well-known/openid-configuration",
        clientId: process.env.DOTS_CLIENT_ID!,
        clientSecret: process.env.DOTS_CLIENT_SECRET!,
        scopes: ["openid", "profile", "email", "offline_access"],
        pkce: true,
      }],
    }),
  ],
});

init

Create the dots client

import { createDots } from "@wrldbld/dots";

export const dots = createDots({
  apiKey: process.env.NEXT_PUBLIC_DOTS_API_KEY!,
  scopes: ["openid", "profile", "email", "offline_access"],
});

auth

Sign in with dots

OAuth 2.1 + PKCE against the dots-hosted Supabase OAuth Server, with branded consent at dots.id. Identity comes from the access-token claims — no extra round-trip.

1. Call the SDK

// kick off sign-in (button, link, anywhere)
await dots.signIn();

// in your /dots/callback route:
const user = await dots.handleCallback();

// anywhere:
const user = await dots.getUser();
// { sub, username, walletAddress, dotsIdStatus, email }
const token = await dots.getAccessToken();
await dots.signOut();

2. Add the callback route

// app/dots/callback/page.tsx
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { dots } from "@/lib/dots";

export default function DotsCallback() {
  const router = useRouter();
  useEffect(() => {
    dots.handleCallback().then(() => router.replace("/"));
  }, [router]);
  return <p>Signing you in…</p>;
}

creation

Two sign-in levels

App sign-in uses Privy plus a one-time wallet confirmation; it does not require Vercel. Choose full dots.id when your app needs the user's public dots.id or colors.bio profile. The activity feed gives you the user-visible event log.

const s = await dots.identity.status();
// { status: "incomplete"|"pending"|"complete",
//   wallet, username, vercel, email, subscription, baseColor }

await dots.identity.confirm({
  signature, message, walletAddress, timestamp,
});

const feed = await dots.identity.activity({ limit: 50 });

credits

pute — one cross-app credit balance

Users carry a single pute balance everywhere. Your app debits it for compute, agent runs, or any metered action. Debits are idempotent via requestId; ok:false means the user is out of credits.

const balance = await dots.credits.balance();

const { ok, balance: left } = await dots.credits.debit(1, {
  requestId: jobId, // idempotent; ok:false ⇒ out of credits
});

const { url } = await dots.credits.topUp({ usd: 3 }); // or { credits: 300 }
// → Stripe Checkout

verified claims

emails + attestations

Verified emails attach to the dots and resolve back to the same user across apps. Attestations carry any verified fact (zkTLS-proven or otherwise) bound to a dots.

await dots.emails.add("me@work.com"); // sends verification link
const emails = await dots.emails.list();

const list = await dots.attestations.list();
// [{ kind, identifier, issuer, verified, verifiedAt }]

await dots.attestations.submitProof(zkTlsProof);

data

Per-user app data

Per-user storage in the user's own database, namespaced to your app. Requires data:read / data:write scopes.

await dots.data.set("prefs", "theme", { mode: "dark" });
const theme = await dots.data.get("prefs", "theme");
const all = await dots.data.list("prefs");

surface

SDK at a glance

dots.signIn() / handleCallback() / signOut()

OAuth 2.1 + PKCE against the dots Supabase OAuth Server.

dots.getUser() / getAccessToken()

Identity from token claims — no extra round-trip.

dots.identity.status() / confirm() / activity()

Drive your onboarding UI off the live creation progress.

dots.credits.balance() / debit() / topUp()

One pute balance, cross-app, idempotent debits.

dots.emails.add() / list()

Verified emails attached to the dots.

dots.attestations.list() / submitProof()

Verified claims (zkTLS or otherwise) bound to the dots.

dots.data.get() / set() / list()

Per-user storage namespaced to your app (data:read/write scopes).

scopes

Choose what your app can sync

openid

Required OIDC identity scope.

profile

Username and basic dots.id profile.

email

Verified email claims.

wallet

Connected wallet address.

social:twitter

Connected Twitter/X username.

social:instagram

Connected Instagram username.

social:google

Connected Google account.

data:read

Read app data from the user's dots data layer.

data:write

Write app data into the user's dots data layer.

pute:read

Read the user's pute balance.

pute:spend

Debit pute on the user's behalf.

pute:topup

Start a checkout to add pute credits.

offline_access

Issue refresh tokens for long-lived sessions.

advanced

No SDK? Use the OAuth endpoints directly

The SDK is a thin wrapper over the dots OAuth 2.1 server. If you're in a language or framework where the SDK doesn't fit, talk to the endpoints directly. Issuer: https://www.dots.id.

Authorize

const params = new URLSearchParams({
  client_id: process.env.DOTS_CLIENT_ID!,
  redirect_uri: process.env.DOTS_REDIRECT_URI!,
  response_type: "code",
  scope: "openid profile email wallet",
  state,
  code_challenge: challenge,
  code_challenge_method: "S256",
});

redirect(`${process.env.DOTS_ISSUER}/api/oauth/authorize?${params}`);

Token exchange

const tokenRes = await fetch(`${process.env.DOTS_ISSUER}/api/oauth/token`, {
  method: "POST",
  headers: { "content-type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    grant_type: "authorization_code",
    code,
    client_id: process.env.DOTS_CLIENT_ID!,
    redirect_uri: process.env.DOTS_REDIRECT_URI!,
    code_verifier: verifier,
  }),
});