Status: proposal, Jul 2026
ampersend can deploy spending accounts for agents, but it has no concept of an end-user. If you're building a product on top of ampersend (like ampersend-simple), you want to create accounts for your users programmatically. Today that means every agent lives under your single platform key, and the user is invisible to ampersend. There is no way to:
- Create a real user identity on ampersend for each of your end-users
- Let a user bring their own wallet instead of using the default embedded one
- Change an agent's spend limits after creation
- Revoke or rotate a session key
- Pause an agent server-side (not just proxy-side)
- Get webhook notifications when payments happen
The Platform API adds these things. The goal is: one API call to create a user and their spending account, get back a credential, and start paying. No dashboards, no approval flows, no redirects.
The Platform API introduces a User between the platform and the agent:
Platform (sk_live_...)
└── User (amp_user_7x...)
├── Agent A (0xABCD...)
│ └── Session Key (0x...)
└── Agent B (0xDEF0...)
└── Session Key (0x...)
A user can own multiple agents. This matters because different tasks need different budgets and permissions. A coding agent should not share a spending account with a research agent.
The platform's existing sk_* key authenticates everything. No new auth
mechanism. ampersend scopes each user and agent to the platform that created
them.
No custody. Session keys are always generated client-side. The platform holds them, not ampersend. ampersend only sees the derived address. This keeps ampersend out of the custody business and means a compromised ampersend server cannot spend user funds.
Multiple agents per user. Users need separate agents for separate workloads. The combined create endpoint accepts an array of agents in a single call.
Three wallet types. Managed (ampersend deploys a smart account, zero config), external (user's own wallet, tracking only), and hybrid (user's wallet routed through ampersend's authorize endpoint for server-enforced policy). All three ship together because the hybrid model is how you get real enforcement on external wallets without giving up flexibility. External-only would force platforms to choose between enforcement and wallet choice, and that's a bad trade-off.
Webhooks from day one. Platforms need to react to payments, budget alerts, and pauses without polling. Simple webhook delivery: register a URL, receive signed POST requests.
Rate limiting. Per-platform limits on user and agent creation, keyed to the platform's plan. Prevents runaway provisioning and makes costs predictable.
All endpoints use Authorization: Bearer sk_*. Amounts are micro-USDC strings
(1 USDC = 1,000,000). Timestamps are unix seconds.
Users are the identity primitive. A user does not need to know ampersend exists.
POST /api/v1/platform/users
Response:
{
"id": "amp_user_7x...",
"external_id": "usr_abc123",
"display_name": "Alice",
"email": "alice@example.com",
"metadata": { "plan": "pro" },
"created_at": 1752422400
}Idempotent on external_id. Calling twice returns the existing user.
GET /api/v1/platform/users/:id
GET /api/v1/platform/users?external_id=usr_abc123
PATCH /api/v1/platform/users/:id (update display_name, email, metadata)
GET /api/v1/platform/users (list, paginated)
DELETE /api/v1/platform/users/:id (soft-delete, pauses all agents)
An agent is a spending account. Each agent gets an on-chain smart account address and a session key for signing payments.
POST /api/v1/platform/users/:user_id/agents
{
"name": "research-agent",
"wallet": { "type": "managed" },
"spend_config": {
"monthly_limit": "20000000",
"daily_limit": "5000000",
"per_transaction_limit": "500000",
"auto_topup_allowed": false
},
"authorized_sellers": null
}Response:
{
"id": "amp_agent_9z...",
"user_id": "amp_user_7x...",
"name": "research-agent",
"address": "0xABCD...",
"status": "deployed",
"wallet_type": "managed",
"session_key": "0x...",
"spend_config": {
"monthly_limit": "20000000",
"daily_limit": "5000000",
"per_transaction_limit": "500000",
"auto_topup_allowed": false,
"monthly_remaining_usdc_micro": "20000000",
"daily_remaining_usdc_micro": "5000000"
},
"balance_usdc_micro": "0",
"created_at": 1752422400
}session_key is returned exactly once, at creation. The platform stores it.
ampersend never sees the private key, only the derived address.
GET /api/v1/platform/users/:user_id/agents
GET /api/v1/platform/users/:user_id/agents/:agent_id
For the common case of "new user, give them a spending account":
POST /api/v1/platform/accounts
{
"user": {
"external_id": "usr_abc123",
"display_name": "Alice",
"email": "alice@example.com"
},
"agents": [
{
"name": "research-agent",
"spend_config": {
"monthly_limit": "20000000",
"per_transaction_limit": "500000"
}
}
]
}Response:
{
"user": {
"id": "amp_user_7x...",
"external_id": "usr_abc123",
"display_name": "Alice",
"email": "alice@example.com",
"created_at": 1752422400
},
"agents": [
{
"id": "amp_agent_9z...",
"address": "0xABCD...",
"session_key": "0x...",
"name": "research-agent",
"wallet_type": "managed",
"spend_config": { ... },
"balance_usdc_micro": "0",
"created_at": 1752422400
}
]
}Idempotent on user.external_id. If the user already exists, their existing
agents are returned alongside any new ones.
The agents array supports multiple entries. Each gets its own smart account
and session key.
Spend limits are frozen at creation today. The Platform API makes them mutable.
Platforms need this because users change their minds. A user starts cautious with $20/month, then wants $100 after they trust the agent. Telling them to create a new account is bad UX.
PATCH /api/v1/platform/agents/:agent_id/spend-config
{
"monthly_limit": "50000000",
"daily_limit": "10000000",
"per_transaction_limit": "1000000",
"auto_topup_allowed": true
}Pass null for any field to remove that limit. Returns the updated config
with current remaining budgets. Takes effect immediately.
Platforms need to rotate keys (scheduled hygiene), revoke them (compromised agent), and freeze accounts (user request, suspicious activity).
POST /api/v1/platform/agents/:agent_id/keys
Returns the new key address. The old key stays valid until explicitly revoked, so the platform can rotate gracefully without downtime.
DELETE /api/v1/platform/agents/:agent_id/keys/:key_address
Immediate. The server stops co-signing for this key. Any in-flight payment using this key will fail.
POST /api/v1/platform/agents/:agent_id/pause
POST /api/v1/platform/agents/:agent_id/unpause
Pause freezes all keys for this agent. The authorize endpoint returns 423 for any payment attempt. This is a real server-side kill switch, not a proxy-level one. Even if the agent has the session key and talks to ampersend directly, it cannot pay.
POST /api/v1/platform/agents/:agent_id/funding-link
{
"amount": "10.00",
"destination": "agent",
"mode": "redirect"
}mode is "redirect" (returns a URL the user visits) or "embed" (returns
a Coinbase on-ramp session token and widget URL for iframe embedding).
Embed mode exists because redirecting users to ampersend's funding page breaks the illusion that your product handles everything. Some platforms want the on-ramp inside their own UI.
POST /api/v1/platform/agents/:agent_id/fund
{
"amount_usdc_micro": "5000000",
"memo": "Welcome bonus"
}For platforms that hold USDC and want to fund users directly. Useful for onboarding credits, referral bonuses, or any case where the platform pays instead of the user.
Platforms need payment data for dashboards, billing reconciliation, and audit
trails. These endpoints mirror what AgentReadClient already exposes, but
authenticated with the platform key so you don't need per-agent SIWE auth.
GET /api/v1/platform/agents/:agent_id/payments?preset=30d
GET /api/v1/platform/agents/:agent_id/activity?preset=30d&limit=50&page=2
GET /api/v1/platform/users/:user_id/payments (aggregated across agents)
Register a URL. ampersend sends signed POST requests when things happen.
POST /api/v1/platform/webhooks
{
"url": "https://yourapp.com/webhooks/ampersend",
"events": [
"payment.completed",
"payment.denied",
"budget.exceeded",
"agent.paused",
"agent.deployed",
"funding.received"
],
"secret": "whsec_..."
}Each webhook payload includes a signature header (X-Ampersend-Signature)
computed with the shared secret. The platform verifies it before processing.
GET /api/v1/platform/webhooks (list)
PATCH /api/v1/platform/webhooks/:id (update URL or events)
DELETE /api/v1/platform/webhooks/:id
Why webhooks instead of just polling: a payment can happen at any time (the agent decides when to spend). The platform needs to update its UI, send the user a notification, or log the event in real time. Polling on a timer wastes requests and adds latency.
ampersend deploys a Safe smart account and generates a session key. Payments are co-signed by ampersend's server through the CoSignerValidator module. Spend policy is enforced server-side. The user never touches crypto.
This is what ampersend does today. It stays the default because it's the simplest integration and has the strongest security guarantees.
// No wallet config needed, managed is the default
const { agents } = await platform.createAccount({
user: { externalId: userId },
agents: [{ name: "my-agent", spendConfig: { monthlyLimit: 20_000_000n } }],
})The platform registers the user's existing wallet address (EOA or any smart wallet). ampersend tracks payments and provides advisory budget alerts but cannot enforce spend limits because it does not co-sign.
This exists because some users already have wallets. Developer-focused platforms especially will have users who want to pay from their own address. Forcing them into a managed account means they have to move funds around, which is friction.
const { agents } = await platform.createAccount({
user: { externalId: userId },
agents: [{
name: "my-agent",
wallet: { type: "external", address: "0x1234..." },
spendConfig: { monthlyLimit: 20_000_000n }, // advisory
}],
})The trade-off is clear: no enforcement. The spend config is tracked and
logged, and ampersend can fire budget.exceeded webhooks, but it cannot block
a transaction the user signs independently.
The user's external wallet routes payments through ampersend's authorize endpoint. ampersend checks the spend policy before co-signing. The user's wallet signs the actual transfer.
This is the answer to "I want my own wallet AND real enforcement." It works because the payment goes through ampersend's authorization layer, just like managed accounts. The difference is that the signing key belongs to the user, not to ampersend.
const { agents } = await platform.createAccount({
user: { externalId: userId },
agents: [{
name: "my-agent",
wallet: {
type: "hybrid",
address: "0x1234...",
chain: "eip155:8453",
},
spendConfig: { monthlyLimit: 20_000_000n }, // enforced
}],
})Hybrid requires the platform to run a pay-proxy (like POST /api/agent/pay
in ampersend-simple) or the user's wallet to support ERC-1271 verification
against ampersend's CoSignerValidator. More integration work than the other
two types, but it's the only way to combine wallet choice with real limits.
It also serves as a backup signing path. If a managed account's session key is compromised, the platform can revoke it and switch the agent to hybrid mode with a fresh external key, without redeploying the smart account.
| Scenario | Wallet type | Why |
|---|---|---|
| Non-technical users, zero config | managed | They don't know what a wallet is |
| Users with existing wallets, trust-based | external | No enforcement needed, just tracking |
| Users with existing wallets, real limits | hybrid | Enforcement without giving up wallet choice |
| Key compromise recovery | hybrid | Revoke managed key, switch to external signer |
The new AmpersendPlatformClient wraps these endpoints:
import { AmpersendPlatformClient } from "@ampersend_ai/ampersend-sdk/ampersend"
import { createAmpersendHttpClient } from "@ampersend_ai/ampersend-sdk/x402"
import { wrapFetchWithPayment } from "@x402/fetch"
const platform = new AmpersendPlatformClient({ apiKey: "sk_live_..." })const { user, agents } = await platform.createAccount({
user: { externalId: "usr_abc123", email: "alice@example.com" },
agents: [{
name: "research-agent",
spendConfig: {
monthlyLimit: 20_000_000n,
perTransactionLimit: 500_000n,
},
}],
})
const agent = agents[0]
// agent.address = "0xABCD..."
// agent.sessionKey = "0x..." (store this, it's only returned once)const paidFetch = wrapFetchWithPayment(fetch,
createAmpersendHttpClient({
smartAccountAddress: agent.address,
sessionKeyPrivateKey: agent.sessionKey,
})
)
const response = await paidFetch("https://paid-api.example.com/joke")const { agents } = await platform.createAccount({
user: { externalId: "usr_abc123" },
agents: [
{
name: "research-agent",
spendConfig: { monthlyLimit: 20_000_000n, perTransactionLimit: 500_000n },
},
{
name: "coding-agent",
spendConfig: { monthlyLimit: 50_000_000n, perTransactionLimit: 2_000_000n },
},
],
})
// agents[0].sessionKey, agents[1].sessionKey ...await platform.agents.updateSpendConfig(agent.id, {
monthlyLimit: 50_000_000n,
dailyLimit: 10_000_000n,
autoTopupAllowed: true,
})const { keyAddress } = await platform.agents.addKey(agent.id)
// deploy the new key to your agent process, then revoke the old one:
await platform.agents.revokeKey(agent.id, oldKeyAddress)await platform.agents.pause(agent.id)
// all keys frozen, authorize endpoint returns 423await platform.webhooks.create({
url: "https://yourapp.com/hooks/ampersend",
events: ["payment.completed", "budget.exceeded"],
secret: "whsec_...",
})const payments = await platform.agents.getPayments(agent.id, { preset: "30d" })
const allUserPayments = await platform.users.getPayments(user.id)const { agents } = await platform.createAccount({
user: { externalId: "usr_abc123" },
agents: [{
name: "my-agent",
wallet: { type: "external", address: "0x1234..." },
spendConfig: { monthlyLimit: 20_000_000n },
}],
})
// spend config is advisory, payments are trackedThe Platform API is additive. Existing AmpersendManagementClient and
AgentReadClient usage keeps working.
| Current | Platform API |
|---|---|
ManagementClient.createAgent(...) |
platform.createAccount(...) |
AgentReadClient.getSelf() |
platform.agents.get(id) or unchanged |
AgentReadClient.getSpendConfig() |
same, plus platform.agents.updateSpendConfig(...) |
ApprovalClient.requestAgentApproval(...) |
still valid for "connect to existing ampersend user" |
| (no equivalent) | platform.users.create(...) |
| (no equivalent) | platform.agents.pause(...) |
| (no equivalent) | platform.agents.addKey(...) / revokeKey(...) |
| (no equivalent) | platform.webhooks.create(...) |
Every /platform/* endpoint is rate limited per platform key.
| Endpoint group | Default limit | Why |
|---|---|---|
| User + agent creation | 100/min | Smart account deployment costs gas, prevent runaway provisioning |
| Reads (users, agents, payments) | 1000/min | Generous for dashboards and reconciliation |
| Writes (spend config, keys, pause) | 200/min | Policy changes should be deliberate, not spammed |
| Webhooks CRUD | 10/min | Rarely changes |
Limits are adjustable per platform plan. 429 responses include a
Retry-After header.
{ "external_id": "usr_abc123", // your user ID, unique per platform "display_name": "Alice", // optional "email": "alice@example.com", // optional "metadata": { "plan": "pro" } // optional, opaque, stored as-is }