# dots.id — admin & dev guide

## Metadata-only control plane

The `admin.dots.id` host is served by this Next.js app at `/ops`. It reads
operational metadata through `/api/ops/control` (Privy + `ADMIN_EMAILS`) and
`/api/admin/control-plane` (server-to-server `DOTS_ADMIN_SECRET`). The API
intentionally selects only account status, username reservations, connection
timestamps, and database health flags. It has no query path for context, files,
notes, prompts, activity payloads, database URLs, or credentials.

Operators sign in with Privy. `ADMIN_EMAILS` is a comma-separated allowlist of
verified profile emails; in production an empty list admits no one.

New usernames can be minted. Names already held from just.is or an admin
invite stay reserved for that email (`reserved_usernames`). Set
`DOTS_INVITE_ONLY_USERNAMES=true` only if you need to close open minting
again. Operators hold a name against an email in `reserved_usernames` (the
same table as the just.is waitlist). That person signs in at `/create` with
that email and the name is claimed automatically.

From admin.dots.id:

1. Enter the username and email, then **hold name**.
2. Copy the claim note and send it to them.
3. If they already have an unnamed account on that email, the name is assigned immediately.

Local: `http://localhost:3000/ops`. Production: `https://admin.dots.id`.

The optional `admin.dots.id/` vinext app can still proxy the same control plane
with `DOTS_ADMIN_SECRET`. Control-plane writes: `check_username` and
`reserve_username`. Repairs still provision/initialize a primary database schema,
reconcile identity connection metadata, or recheck a username against Firebase
live. None return synchronized rows.

## Granting pute credits (dev-admin)

Fund any dot's credit balance server-to-server. Used for testing, comps, and refunds.

```bash
# grant 500 credits
curl -X POST "$BASE_URL/api/admin/pute/grant" \
  -H "Authorization: Bearer $DOTS_ADMIN_SECRET" \
  -H "content-type: application/json" \
  -d '{ "privyUserId": "did:privy:...", "amount": 500, "reason": "grant", "requestId": "comp-2026-05-26-001" }'

# read a balance
curl "$BASE_URL/api/admin/pute/grant?privyUserId=did:privy:..." \
  -H "Authorization: Bearer $DOTS_ADMIN_SECRET"
```

- **Idempotent** on `requestId` — safe to retry; a repeated `requestId` won't double-credit.
- `reason` ∈ `grant | topup | refund | adjust` (default `grant`).
- Requires `DOTS_ADMIN_SECRET` to be set. Keep it server-side; never expose it to a browser or the SDK.

## Self-serve top-up (Stripe)

`POST /api/pute/topup { credits }` (user-authed) returns a Stripe Checkout URL. On
payment, the **existing subscribe webhook** (`/api/identity/subscribe/webhook`,
`checkout.session.completed` with `metadata.type=pute_topup`) credits the account,
idempotent on the Stripe session id. Configure `PUTE_CREDITS_PER_USD` (default 100)
and `PUTE_TOPUP_DEFAULT_CREDITS` (default 1000).

## Managing apps / API keys

The dev portal at `/dev/clients` (API: `/api/dashboard/clients`) lets owners create
and manage OAuth clients (API keys). See [Getting an API key](./GETTING_AN_API_KEY.md).

## Smoke test

With a server running, verify the credit + identity paths end to end:

```bash
BASE_URL=https://dots.localhost \
DOTS_ADMIN_SECRET=... \
TEST_PRIVY_USER_ID=did:privy:... \
TEST_USER_TOKEN=...   # optional — also checks status/balance/debit/attestations \
bun scripts/smoke-dots.ts
```

## Unit tests

```bash
bun test            # all (SDK + libs + route gates)
cd packages/sdk && bun test   # SDK only
```

## zkTLS / co-sign (alpha)

Co-signing requires a verified zkTLS proof as a second factor. Until a provider
(Reclaim) is wired, set `DOTS_DEV_ALLOW_UNVERIFIED_COSIGN=true` **in dev only** to
exercise the flow — accepted proofs are recorded as `method='dev-bypass'` (never
`zktls`) so they're always auditable. Never enable this in production.