Platform API (Multi-Tenant)
The Platform API lets you build products on top of 1claw. You register a platform app, define a bootstrap template, and provision 1claw resources (vaults, agents, policies, signing keys) for each of your end users. Your users get secure key management and signing without you building the infrastructure.
The Platform API requires a Pro or higher subscription. The dashboard shows an upgrade prompt if you are on the free tier.
How it works
Your SaaS 1claw Platform API End user
| | |
| POST /v1/platform/apps | |
| (register your app) | |
| --------------------------------> | |
| | |
| POST /v1/platform/users/upsert | |
| (provision a user) | |
| --------------------------------> | |
| | |
| POST /v1/platform/ | |
| connections/{id}/bootstrap | |
| (apply template: vault + agent | |
| + policies + signing keys) | |
| --------------------------------> | |
| | |
| <-- claim_url, agent_api_key --- | |
| | |
| Send claim URL to end user ---> | ----------------------> |
| | User claims resources |
- Register a platform app and get a
plt_API key. - Create a bootstrap template that defines what each user gets (vault, agent, policies, signing keys).
- When a user signs up for your product, provision them via the Platform API.
- Bootstrap applies the template: creates a vault, agent, access policies, and optionally signing keys.
- The user claims the resources via a one-time URL (or silently, depending on auth mode).
Step 1: Register a platform app
- curl
- TypeScript
APP=$(curl -s -X POST https://api.1claw.co/v1/platform/apps \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "My Trading Platform",
"slug": "my-trading-platform",
"description": "Automated trading platform built on 1claw",
"billing_model": "platform_pays",
"auth_mode": "silent"
}')
PLT_KEY=$(echo "$APP" | jq -r '.api_key')
APP_ID=$(echo "$APP" | jq -r '.id')
echo "Platform key: $PLT_KEY" # save this, shown once
const app = await client.platform.createApp({
name: "My Trading Platform",
slug: "my-trading-platform",
description: "Automated trading platform built on 1claw",
billing_model: "platform_pays",
auth_mode: "silent",
});
const platformKey = app.data.api_key; // save, shown once
const appId = app.data.id;
Auth modes
| Mode | Behavior |
|---|---|
silent | User is provisioned without interaction. Your OIDC token is sufficient. |
user_signin | User must sign in to 1claw to claim resources. |
configurable | You choose per connection. |
Billing models
| Model | Who pays |
|---|---|
platform_pays | Your org's subscription covers all user resources (default). |
user_pays | Each end user's subscription covers their resources. |
hybrid | You pay the base; users pay overages. |
Step 2: Create a bootstrap template
Templates define what gets created for each user:
- curl
- TypeScript
curl -X POST "https://api.1claw.co/v1/platform/apps/$APP_ID/templates" \
-H "Authorization: Bearer $PLT_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "trader-setup",
"description": "Standard trader setup with vault, agent, and Ethereum signing key",
"spec": {
"vault": {
"name": "{{user_name}} Trading Vault",
"description": "Secrets and keys for automated trading"
},
"agents": [
{
"name": "{{user_name}} Trading Bot",
"description": "Automated trading agent",
"intents": { "enabled": true },
"shroud_enabled": true,
"signing_keys": [
{ "chain": "ethereum" },
{ "chain": "solana" }
]
}
],
"policies": [
{
"vault_ref": 0,
"principal_ref": "agent:0",
"paths": ["keys/*", "config/*"],
"permissions": ["read"]
}
]
}
}'
await client.platform.createTemplate(appId, {
name: "trader-setup",
description: "Standard trader setup",
spec: {
vault: {
name: "{{user_name}} Trading Vault",
description: "Secrets and keys for automated trading",
},
agents: [
{
name: "{{user_name}} Trading Bot",
description: "Automated trading agent",
intents: { enabled: true },
shroud_enabled: true,
signing_keys: [{ chain: "ethereum" }, { chain: "solana" }],
},
],
policies: [
{
vault_ref: 0,
principal_ref: "agent:0",
paths: ["keys/*", "config/*"],
permissions: ["read"],
},
],
},
});
The template spec supports:
| Field | Description |
|---|---|
vault | Name and description for the user's vault |
agents[] | Array of agents to create (each with optional Intents API, Shroud, signing keys) |
agents[].signing_keys[] | Per-chain signing keys provisioned at bootstrap (ethereum, bitcoin, solana, xrp, cardano, tron) |
agents[].provision_eoa | Set true to generate a standalone EOA for client-side smart account deployment |
policies[] | Access policies linking vaults, agents, and paths |
Per-agent capability flags that a template can set:
| Field | Description |
|---|---|
intents_api_enabled / intents | Intents API. Accepts true, "true", or { "enabled": true } |
execution_intents_enabled / execution | Execution Intents |
shroud_enabled, shroud_config | Shroud LLM proxy |
memory_enabled / memory | Agent memory |
raw_signing_enabled | Raw/blind digest signing (sign_digest, eip712_digest) |
message_signing_enabled | personal_sign message signing |
environment, environment_locked | Environment pinning |
default_llm_model, system_prompt | LLM defaults |
What a template cannot set
More than one agent in agents[]: a template declaring two or more is refused
with 400. See One agent per call below for why.
Spend caps, signing policies and the other guardrails (tx_max_value,
tx_daily_limit, raw_signing_policy, pay_*, card_*, usdc_daily_limit_usd,
delegation_enabled, …) cannot be set from a template spec. Including one
returns 400 naming the field.
That refusal is deliberate. A template spec is stored as-is, so a guardrail it named used to be accepted and then silently absent on the provisioned agent — handing back an agent with no cap to a caller who believed it was capped. Failing loudly at template-write time is the safe direction.
Set them per agent after bootstrap, using the agent_ids in the bootstrap
summary:
await platform.agents.update(bootstrap.data.summary.agent_ids[0], {
tx_max_value: "0.1",
raw_signing_policy: "approve",
});
Adding agents after bootstrap
Bootstrap runs once per connection. A second call returns 409 Connection has already been bootstrapped, because bootstrap also creates the vault, the
policies and the plan tier, and none of those should happen twice.
To add an agent to a tenant later, post a template to the connection:
const { data } = await platform.platform.addConnectionAgents(connectionId, {
template_id: templateId,
params: { account: "0xabc..." },
});
// data.agent_ids, data.signing_keys
// data.agent_api_keys[0].api_key — returned once, never retrievable again
One agent per call. A template declaring more than one agent is refused with
400, both when the template is written and when it is used. The reason is the
API key: it is returned once and cannot be re-issued to a platform app, because
rotate_agent_key only works for a caller in the agent's own org and your
plt_ key is not. A response carrying several keys would put several
unrecoverable credentials in one payload, so one leaked log line would take out
every agent in the batch with no way to rotate any of them.
For N agents, use N single-agent templates and call once per agent. Each call
then gets its own params, its own Idempotency-Key, and its own key handed
back cleanly.
Only the agent part of the template is applied: its agents and their
per-agent signing_keys, eoa and runtime. A vault, policies,
automations or entitlements block in the same spec is ignored, and the new
agents attach to the vault the connection already has. That is why the
configuration comes from a template rather than from fields on the request:
the agent portion of a spec has one reader, shared with bootstrap, so a
capability cannot work in one and not the other.
The number of agents on a connection is capped by
max_agents_per_connection on the platform app. Unset means a built-in
default rather than unlimited, because an agent provisioned with signing keys
costs a KMS key per chain. The ceiling is checked before anything is created,
and the refusal names the current count and the limit.
The call is idempotent on Idempotency-Key: a retry replays the original
response rather than provisioning a second agent, and a key reused with
different parameters is refused with 409.
Step 3: Provision users
When a user signs up for your product:
// 1. Upsert the user
const connection = await platformClient.platform.upsertUser({
email: user.email,
});
// 2. Bootstrap their resources from the template
const bootstrap = await platformClient.platform.bootstrapUser(
connection.data.connection_id,
{ template_name: "trader-setup" },
);
console.log("Vault ID:", bootstrap.data.summary.vault_id);
console.log("Agent ID:", bootstrap.data.summary.agent_id);
console.log("Agent API key:", bootstrap.data.summary.agent_api_key); // one-time
console.log("Signing keys:", bootstrap.data.summary.signing_keys);
// signing_keys: [{ chain: "ethereum", address: "0x...", public_key: "..." }, ...]
// 3. Send claim URL to the user (if auth_mode is "user_signin")
if (bootstrap.data.claim_url) {
await sendEmail(user.email, {
subject: "Claim your trading account",
body: `Click here to claim: ${bootstrap.data.claim_url}`,
});
}
OIDC user provisioning
If your users already authenticate through an OIDC provider, pass their JWT directly:
const connection = await platformClient.platform.upsertUser({
subject_token: userJwt, // validated against your app's oidc_jwks_url
});
Configure oidc_jwks_url and oidc_issuer on your platform app to enable JWT validation.
Step 4: Use the bootstrapped agent
Once bootstrapped, the agent API key works like any other 1claw agent:
import { createClient } from "@1claw/sdk";
// Client using the bootstrapped agent's API key
const tradingAgent = createClient({
baseUrl: "https://api.1claw.co",
apiKey: bootstrapResult.summary.agent_api_key,
});
// Submit a trade
const tx = await tradingAgent.agents.submitTransaction(agentId, {
chain: "ethereum",
to: "0xUniswapRouter...",
value: "0",
data: swapCalldata,
simulate_first: true,
});
Custody guarantee
When platform_locked = true (set automatically during bootstrap), the platform operator (you) cannot read secret values. You can manage lifecycle (create, delete, rotate) but cannot access the raw key material. This protects your end users' secrets from your own backend.
Claim flow
For user_signin mode, the end user visits the claim URL:
- The claim page shows your app name, provisioned resources (vaults, agents, policies), and a custody guarantee
- The user clicks "Claim Resources"
- The connection status changes from
pendingtoclaimed
If the claim token expires (10 minutes), reissue it:
const reissue = await platformClient.platform.reissueClaim(connectionId);
console.log("New claim URL:", reissue.data.claim_url);
User grants (resource sharing)
After claiming, users can grant your platform app access to specific vaults and agents:
// User grants access to their vault
const grant = await userClient.platform.grantAccess(connectionId, {
vault_ids: [vaultId],
agent_ids: [agentId],
allowed_paths: ["config/*"],
permissions: ["read"],
});
Users can revoke grants at any time from the dashboard (Settings > Connected Apps).
Dashboard
The Platform section in the dashboard provides:
- App management: create, edit, delete platform apps
- Template editor: visual template builder (no raw JSON needed)
- Connected users: list of provisioned users with claim status
- Audit log: platform-specific events (
platform.user_provisioned,platform.claim_redeemed, etc.)
Further reading
- Platform API reference for the full API documentation
- Embedded wallets quickstart for wallet-in-your-app patterns
- @1claw/wallet-react for the embeddable React widget
- Billing for tier details and platform billing