Masumi SaaS is a platform for registering, managing, and verifying your AI agents on the Masumi network. Users can register agents, complete identity verification (KYC), manage organizations, and top up or withdraw funds.
- Next.js 16 (App Router)
- React 19
- TypeScript
- Prisma (PostgreSQL)
- Better Auth (with organization and API key plugins)
- Zod (Validation)
- Tailwind CSS + shadcn/ui
- next-themes (Theme switching)
- next-intl (Internationalization)
- Sentry (Error tracking)
This project uses a hybrid architecture depending on the feature:
- Config (
apps/web/src/lib/config/) - Centralized environment configuration (app, auth, email, sumsub, veridian) - Server Actions (
apps/web/src/lib/actions/) - Directly call Prisma and external services (payment node, Veridian) - API Routes (
apps/web/src/app/api/) - HTTP endpoints for client-side fetching and public API - API Clients (
apps/web/src/lib/api/) - Client-side fetch wrappers for API routes - Payment Node (
apps/web/src/lib/payment-node/) - Typed HTTP client + per-user key encryption for Masumi payment node - Services (
apps/web/src/lib/services/) - Business logic and data access (Prisma) - Schemas (
apps/web/src/lib/schemas/) - Shared Zod schemas for API routes and server actions
Agent registration flow: Server Action → Payment Node Client → Prisma (wallets generated server-side, no API route involved)
Dashboard/other data flow: Server Component / Action → API Client → API Route → Service → Prisma
API authentication: Authenticated API routes (/api/agents, /api/dashboard/overview, /api/credentials/*, etc.) accept either a session cookie (browser) or an API key: Authorization: Bearer <key> or x-api-key: <key>.
Authenticated routes require either:
-
Session cookie – used by the browser when you’re logged in.
-
API key – for CLIs, MCP, scripts, and server-to-server calls. Create keys in the app under API Keys. Send the key in one of two ways:
# Option 1: Authorization header (recommended) curl -H "Authorization: Bearer YOUR_API_KEY" https://your-domain.com/api/agents # Option 2: x-api-key header curl -H "x-api-key: YOUR_API_KEY" https://your-domain.com/api/agents
Unauthenticated requests receive 401 Unauthorized with {"success":false,"error":"Unauthorized"}.
Masumi SaaS can also act as an OIDC issuer for external apps and SpacetimeDB:
Detailed handoff doc for the external webapp repo:
-
Discovery:
GET /.well-known/openid-configuration -
OAuth metadata:
GET /.well-known/oauth-authorization-server -
JWKS:
GET /jwks -
OIDC auth endpoints:
/api/auth/oauth2/* -
CLI device verification UI:
/device -
Device authorization endpoint:
POST /api/auth/device/code
Trusted first-party client IDs default to:
masumi-spacetime-webmasumi-spacetime-cli
SpacetimeDB reducers should validate:
iss === <your public issuer>audcontains one of the trusted client IDs above- signature against Masumi JWKS, which now publishes
ES256keys for SpacetimeDB compatibility
If you are upgrading from an older local setup that issued EdDSA tokens, clear or recreate the auth database so Better Auth generates a fresh ES256 JWK. Better Auth will otherwise continue reusing the latest stored signing key.
For browser flows that authenticate directly against Better Auth, use POST /api/oidc/spacetimedb/token to exchange the current authenticated browser session for an issuer-signed OIDC token set suitable for SpacetimeDB. This bridge is session-cookie only and rejects API-key or bearer-token callers. The bridge accepts origins configured via OIDC_WEB_REDIRECT_URLS in addition to CORS_ALLOWED_ORIGINS. Request body:
{ "client": "web" }or
{ "client": "cli" }For CLI sign-in, request a device code from POST /api/auth/device/code, approve it via /device / /device/approve, and poll the standard token endpoint POST /api/auth/oauth2/token with grant_type=urn:ietf:params:oauth:grant-type:device_code to receive the OIDC token set (access_token, id_token, optional refresh_token) directly. The legacy alias POST /api/auth/device/token remains supported for compatibility, but new clients should use /api/auth/oauth2/token.
Refresh-token exchanges also return a fresh id_token, so claims such as email_verified can change on refresh without requiring a full re-login. Each issued id_token includes a unique jti; tokens in the same refresh chain share a stable sid.
For external OIDC clients, Masumi SaaS also accepts Authorization: Bearer <access_token> on the scoped API routes. Standard identity scopes remain:
openidprofileemailoffline_access
Current Masumi API permission scopes are:
agents:read:preprod,agents:write:preprod,agents:read:mainnet,agents:write:mainnetcredentials:read:preprod,credentials:write:preprod,credentials:read:mainnet,credentials:write:mainnetactivity:read:preprod,activity:read:mainnetearnings:read:preprod,earnings:read:mainnetdashboard:read:preprod,dashboard:read:mainnet
Granted API scopes in an issued access_token are:
requested scopes ∩ stored user grants ∩ client allowlist
| Path | Description |
|---|---|
GET/POST /api/agents |
List or register agents |
GET/DELETE /api/agents/[id] |
Get or delete an agent |
GET /api/agents/counts |
Agent counts by status and network |
GET /api/dashboard/overview |
User, KYC, orgs, agents, API keys, balance |
GET /api/earnings |
Earnings and payouts |
GET/POST /api/credentials/* |
Veridian credentials (issue, status, reconcile, etc.) |
The v1 namespace exposes read-only, rate-limited endpoints for agent discovery. No API key required.
| Path | Description |
|---|---|
GET /api/v1/agents |
List agents by verification status. Query: status (PENDING, VERIFIED, REVOKED, EXPIRED; default VERIFIED), page, limit. |
GET /api/v1/agents/[id] |
Get a single agent by ID. |
GET /api/v1/agents/verify |
Verify an agent identifier and return current credential status. |
OpenAPI JSON for this surface: GET /api/v1/openapi (Swagger UI: /docs/openapi).
Platform HTTP API (session or API key): OpenAPI JSON at GET /api/openapi (Swagger UI: /docs/saas-openapi). Describes explicit runtime paths like /api/agents, /api/dashboard/*, /api/credentials/*, plus allow-listed /pay/api/v1/* and /registry/api/v1/* wrapper paths — not the public catalog above.
Documentation: header Documentation opens the Masumi DevHub. /docs redirects there with 307 (temporary) so browsers/CDNs do not cache a permanent hop if the external docs URL changes. Developers (signed-in) → /developers: Schema Validator and OpenAPI; OpenAPI iframe is /docs/saas-openapi. Public discovery: /docs/openapi. Old paths /docs/api and /docs/saas-api 308 to /docs/saas-openapi.
Example:
curl "https://your-domain.com/api/v1/agents?status=VERIFIED&limit=10"The authenticated v1 namespace proxies a curated set of routes to external Masumi services. Route exposure is generated from checked-in upstream OpenAPI specs and matched on method + normalized path. New upstream routes stay 403 until added to the safe manifest. Use app authentication (session or API key); SaaS forwards either the user's payment-service token or a shared registry-service token server-side. Paths containing traversal or malformed segments are rejected before fetch.
| Path | Description |
|---|---|
GET/POST /pay/api/v1/payment |
Create or list payments |
GET /pay/api/v1/payment-source |
List payment sources |
GET/POST/DELETE /pay/api/v1/registry |
Register agents, list registry, deregister |
GET/POST /pay/api/v1/inbox-agents |
List or register inbox agents |
DELETE /pay/api/v1/inbox-agents/{id} |
Delete an inbox agent entry |
POST /pay/api/v1/inbox-agents/{id}/deregister |
Deregister an inbox agent |
POST /registry/api/v1/registry-entry |
Query registry-service agent lookup entries |
POST /registry/api/v1/registry-entry-search |
Search registry-service agent lookup entries |
POST /registry/api/v1/registry-diff |
Query registry-service diffs |
GET /registry/api/v1/payment-information |
Payment information for one agent |
GET /registry/api/v1/capability |
Registry-service capability lookup |
POST /registry/api/v1/inbox-agent-registration |
Inbox agent registration lookup |
POST /registry/api/v1/inbox-agent-registration-diff |
Inbox registration diffs |
POST /registry/api/v1/inbox-agent-registration-search |
Inbox registration search |
GET/POST/PATCH/DELETE /registry/api/v1/registry-source |
Manage registry sources (admin only) |
| … | See the generated proxy manifest |
Implementation: apps/web/src/app/pay/api/v1/*, apps/web/src/app/registry/api/v1/*, and apps/web/src/lib/v1-proxy/manifest.ts.
Regenerate checked-in OpenAPI JSON (same workflow as masumi-payment-service pnpm run swagger-json): from the monorepo root run pnpm --filter web run swagger-json (alias: swagger:generate). Writes:
apps/web/src/lib/swagger/openapi-docs.json— public v1 discovery spec (generator.ts)apps/web/src/lib/swagger/openapi-platform-docs.json— platform HTTP API spec (saas-app-openapi.ts)apps/web/public/openapi.json— copy of the v1 spec for static hosting
GET /api/v1/openapi and GET /api/openapi still build the spec at request time; commit the JSON when you change the Zod registries so diffs are reviewable.
To regenerate the payment node client: pnpm --filter web run payment-node:generate (fetches latest OpenAPI from https://payment.masumi.network/api-docs, then runs openapi-typescript). Override the spec URL: PAYMENT_NODE_OPENAPI_URL=https://your-host/api-docs pnpm --filter web run payment-node:fetch-spec. To typegen only from the committed JSON (offline): pnpm --filter web run payment-node:generate:local.
To regenerate the registry service client: pnpm --filter web run registry-service:generate (fetches latest OpenAPI from https://registry.masumi.network/api-docs, then runs openapi-typescript). Override the spec URL: REGISTRY_SERVICE_OPENAPI_URL=https://your-host/api-docs pnpm --filter web run registry-service:fetch-spec. To typegen only from the committed JSON (offline): pnpm --filter web run registry-service:generate:local.
masumi-saas/
├── apps/
│ └── web/ # Next.js application
│ ├── src/
│ │ ├── app/ # App Router routes
│ │ │ ├── (app)/ # Authenticated routes
│ │ │ │ ├── ai-agents/ # Agent management
│ │ │ │ ├── organizations/ # Organization management
│ │ │ │ ├── account/ # User account
│ │ │ │ ├── onboarding/ # KYC flow
│ │ │ │ ├── top-up/ # Add funds
│ │ │ │ └── withdraw/ # Withdraw earnings
│ │ │ ├── (auth)/ # Authentication routes
│ │ │ └── api/ # API routes
│ │ │ ├── agents/ # Agent CRUD (authenticated)
│ │ │ ├── dashboard/ # Dashboard overview (authenticated)
│ │ │ ├── credentials/ # Veridian credentials (authenticated)
│ │ │ ├── v1/ # Public API (agents, openapi; no auth)
│ │ │ └── webhooks/ # External webhooks
│ │ ├── components/ # UI components
│ │ ├── lib/
│ │ │ ├── config/ # Env config (app, auth, email, sumsub, veridian)
│ │ │ ├── actions/ # Server actions (agent, auth, organization)
│ │ │ ├── api/ # API clients (agent, dashboard, credential)
│ │ │ ├── payment-node/ # Masumi payment node client + encryption
│ │ │ ├── services/ # Business logic
│ │ │ ├── types/ # Shared TypeScript types
│ │ │ ├── schemas/ # Zod schemas
│ │ │ ├── utils/ # Shared utilities
│ │ │ └── auth/ # Better Auth setup
│ │ └── ...
│ └── messages/ # i18n messages
├── packages/
│ └── database/ # Shared database layer
│ ├── prisma/
│ │ ├── schema.prisma # Database schema
│ │ └── migrations/
│ └── src/
│ └── client.ts # Prisma client
└── package.json # Root workspace config
-
Install dependencies:
pnpm install
-
Set up environment variables:
Configure environment variables in
apps/web/.env.cp apps/web/.env.example apps/web/.env
Edit
apps/web/.envwith the following values:-
DATABASE_URL: Your PostgreSQL connection string
- Format:
postgresql://username:password@host:port/database?schema=public
- Format:
-
BETTER_AUTH_SECRET: A random secret key for signing session tokens
- Generate one with:
openssl rand -base64 32
- Generate one with:
-
BETTER_AUTH_URL: Your application's base URL
- For local development:
http://localhost:2999
- For local development:
-
OIDC_PUBLIC_ISSUER_URL (optional): Public OIDC issuer URL
- Defaults to
BETTER_AUTH_URL
- Defaults to
-
OIDC_WEB_CLIENT_ID / OIDC_WEB_REDIRECT_URLS (optional): Trusted public OIDC client for the external webapp
- Local default redirect:
http://localhost:3002/auth/callback(avoids clashing with this app on port 2999)
- Local default redirect:
-
OIDC_CLI_CLIENT_ID / OIDC_CLI_REDIRECT_URLS (optional): Trusted public OIDC client for the CLI device flow
- Local default redirect:
http://127.0.0.1:43110/callback
- Local default redirect:
-
OIDC_DEVICE_VERIFICATION_URI (optional): OIDC device flow verification page path or absolute URL
- Defaults to
/device
- Defaults to
-
NEXT_PUBLIC_APP_URL: Full base URL for server-side API calls (optional)
- Falls back to request headers if not set
-
NEXT_PUBLIC_SOKOSUMI_MARKETPLACE_URL: Sokosumi marketplace base URL (optional)
- Defaults to
https://app.sokosumi.com
- Defaults to
-
POSTMARK_SERVER_ID / POSTMARK_FROM_EMAIL / POSTMARK_FROM_NAME: Postmark sender config (optional)
- If not set, emails are logged to console in development
- Uses per-email sender names like
Masumi Verification <support@masumi.network>andAgent Messenger <support@masumi.network>for OIDC magic links - Rewrites
no-reply/noreplylocal parts tosupport@...
-
EMAIL_BRAND_LOGO_URL (optional): Absolute URL of the Masumi logo used in transactional emails and the "Powered by Masumi" footer in Agent Messenger OIDC emails. Defaults to the app logo asset if unset.
-
EMAIL_AGENT_MESSENGER_LOGO_URL (optional): Absolute URL of the logo shown at the top of Agent Messenger OIDC magic-link emails. Defaults to
EMAIL_BRAND_LOGO_URLif unset. -
NEXT_PUBLIC_PRIVACY_POLICY_URL (optional): Privacy policy URL used by signup forms (checkbox link) and the consent line in magic-link emails when the address is not yet registered. Defaults to
https://www.masumi.network/privacyif unset. -
NEXT_PUBLIC_SENTRY_DSN / SENTRY_AUTH_TOKEN / SENTRY_PROJECT: Sentry config (optional)
-
SUMSUB_APP_TOKEN / SUMSUB_SECRET_KEY: Sumsub credentials (optional, for KYC/KYB)
-
SUMSUB_BASE_URL: Defaults to
https://api.sumsub.com -
SUMSUB_KYC_LEVEL / SUMSUB_KYB_LEVEL: Verification level names (default:
"id-only") -
VERIDIAN_CREDENTIAL_SERVER_URL: Veridian credential server URL (optional)
-
VERIDIAN_KERIA_URL: KERIA connect URL (optional, use port 3901 not 3903)
-
VERIDIAN_AGENT_VERIFICATION_SCHEMA_SAID: Schema SAID for agent verification credentials
-
PAYMENT_NODE_BASE_URL: Base URL of the Masumi payment node API, including the version path
- e.g.
https://payment.masumi.network/api/v1 - local app-only dev may use
http://localhost:2999/api/v1; startup validation now skips that self-proxy so boot does not hang
- e.g.
-
PAYMENT_NODE_ADMIN_API_KEY: Admin API key for the payment node (server-side only, never exposed to client)
- Used to generate wallets and create per-user API keys
-
PAYMENT_NODE_PAYMENT_SOURCE_ID: Shared payment source ID for adding wallets
-
PAYMENT_NODE_REGISTRATION_FUNDING_WALLETS_PREPROD / PAYMENT_NODE_REGISTRATION_FUNDING_WALLETS_MAINNET: Comma-separated managed selling wallet addresses on that payment source used to fund agent registration transactions
-
PAYMENT_NODE_ENCRYPTION_KEY: Encryption key for storing per-user payment node API keys (min 32 chars)
- Generate with:
openssl rand -base64 32
- Generate with:
-
PAYMENT_NODE_STRICT_STARTUP (optional): Set to
1to throw on startup if payment node is unreachable -
REGISTRY_SERVICE_BASE_URL: Base URL of the Masumi registry service API, including the version path
- e.g.
https://registry.masumi.network/api/v1
- e.g.
-
REGISTRY_SERVICE_API_KEY: Shared server-side API key for safe registry lookup and discovery proxy routes
- Used for
/registry/api/v1/registry-entry, capability lookup, inbox registration lookup, and similar read-only registry routes
- Used for
-
REGISTRY_SERVICE_OPENAPI_URL (optional): Override URL for fetching the registry service OpenAPI document during type generation
- Defaults to
https://registry.masumi.network/api-docs
- Defaults to
-
-
Configure Sumsub Webhook (required for automatic KYC status updates):
- Go to your Sumsub Dashboard → Settings → Webhooks
- Add a webhook pointing to
https://yourdomain.com/api/webhooks/sumsub - For local development, use ngrok to expose your local server
-
Set up the database:
# Generate Prisma client pnpm prisma:generate # Run migrations pnpm prisma:migrate:dev
-
Start the development server:
pnpm dev
The project includes a CLI tool for managing admin users.
-
Sign up normally at
/signup -
Run the promote command:
pnpm admin:promote your@email.com
pnpm admin:promote user@example.com # Promote to admin
pnpm admin:demote user@example.com # Demote to regular user
pnpm admin:list # List all adminsAfter promoting, admins can sign in at /admin/signin.
- ✅ User authentication (email/password, social sign-in, forgot password, 2FA)
- ✅ Organization management (multi-tenant, org dashboard)
- ✅ API key authentication – Use API keys for CLIs, MCP, and scripts (
Authorization: Bearerorx-api-key) - ✅ Public API (v1) – Unauthenticated, rate-limited agent listing and OpenAPI spec
- ✅ Dashboard – Overview with balance, agents, organizations; top up & withdraw
- ✅ AI Agent management – Register, verify, and manage agents on the Masumi network
- ✅ Payment node integration – Wallet generation, agent registration via Masumi payment node; Preprod/Mainnet network toggle
- ✅ KYC/KYB – Identity verification via Sumsub
- ✅ Veridian integration – Cryptographic credentials for agent verification
- ✅ Cookie consent banner
- ✅ Error tracking with Sentry
- ✅ Dark/light theme (auto-detect)
- ✅ Responsive design
- ✅ Server-side rendering with Suspense + skeleton loading
Masumi SaaS follows the same promotion model as the payment service:
- Feature branches → open PRs against
dev(staging). devis the integration branch — CI runs on PRs here before changes are exercised on staging.mainis production — onlydevmay merge intomain(enforced in CI).
Direct pushes to main and dev are blocked locally by the pre-push hook and should be blocked on GitHub via branch protection.
pnpm dev- Start development serverpnpm build- Build for productionpnpm start- Start production serverpnpm lint- Run ESLintpnpm format- Format code with Prettierpnpm prisma:generate- Generate Prisma clientpnpm prisma:migrate:dev- Run database migrationspnpm prisma:studio- Open Prisma Studiopnpm admin:promote <email>- Promote user(s) to adminpnpm admin:demote <email>- Demote admin(s) to regular userpnpm admin:list- List all admin users
- Email/Password Authentication: Sign up and sign in with email and password
- Magic link: Passwordless sign-in link; new email addresses receive a short Privacy Policy consent line in the email body (existing accounts do not). Display names default from the email local part when no name is provided.
- Organization Plugin: Multi-tenant support with organizations, members, and invitations
- API Key Plugin: Generate and manage API keys; use them to authenticate API routes (
Authorization: Bearerorx-api-keyheader) with rate limiting - Bearer Plugin: Session-token authentication for cross-domain clients and device flows
- OIDC Provider: Public issuer metadata, JWKS, trusted first-party public clients, and JWT-signed
id_tokens for SpacetimeDB withjtiandsidclaims - Device Authorization: CLI login with
/api/auth/device/code,/api/auth/oauth2/token,/device, and/device/approve; token polling returns OIDC tokens directly, and the legacy/api/auth/device/tokenalias remains available - Two-Factor Authentication: TOTP-based 2FA support
- Localization: Built-in support for multiple languages
Sentry is configured for:
- Server-side error tracking
- Client-side error tracking
- Edge runtime error tracking
- Source map uploads (in production)
- Session replay (1% of sessions, 100% on errors)