LenlyPay API
Human-to-agent payment rails. Agents request payments; the Seal decides; approved payments settle on-chain.
Overview
An operator (you) creates agents. Each agent gets a wallet with a delegated signer that lives encrypted on the LenlyPay server. You attach a Seal — a spending policy — and hand your agent an API key. From then on the agent calls one endpoint to pay for things. Every request is validated against the Seal before anything is signed; the response tells the agent exactly why a payment was approved or blocked.
Human wallet (root) → Seal (policy) → Agent wallet (scoped signer) → Payment
Concepts
- Human account
- The root authority. Signs in with email or a wallet signature, funds agents, creates and revokes Seals, freezes agents.
- Agent wallet
- An independent account per agent (
agent_…). Its signing secret is generated server-side, encrypted with AES-256-GCM and never returned by any endpoint. - Seal
- A policy with allowed networks and assets, per-transaction / daily / monthly USD limits, recipient mode (anyone or whitelist), contract allowlist, expiry, gas cap and an emergency-freeze switch. One active Seal per agent.
- Session permission
- The grant that binds the agent signer to a specific Seal. Attaching, revoking or rotating re-issues it; without an active permission the signer cannot be used.
- Payment
- A request with a full receipt: every policy check, the decision, the transaction hash and the explorer link. Statuses:
PENDING · SETTLED · BLOCKED · FAILED.
Authentication
Every /api/v1 request carries an API key created on the API Keys page. Keys look like lp_live_… or lp_test_…; only their SHA-256 hash is stored.
Authorization: Bearer lp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Errors come back as { "error": { "code", "message" } } with codes UNAUTHORIZED · VALIDATION · NOT_FOUND · RATE_LIMITED · PROVIDER_NOT_CONFIGURED · INTERNAL.
POST /api/v1/payments
Request a payment on behalf of an agent. The server authenticates the key, loads the agent and its Seal, validates, and only then signs and broadcasts.
POST /api/v1/payments
{
"agentId": "agent_…",
"recipient": "0x…", // EVM address
"amount": "2.5", // decimal string in asset units
"asset": "USDC", // symbol configured for the chain
"chainId": 8453, // optional, defaults to the agent's network
"contractAddress": "0x…", // optional, must be in the Seal's contract allowlist
"metadata": { "job": "…" } // optional, stored on the receipt
}201 Created (SETTLED) · 202 Accepted (PENDING) · 403 (BLOCKED) · 502 (FAILED)
{
"id": "pay_…",
"object": "payment",
"status": "SETTLED",
"policy": "APPROVED",
"reason": "APPROVED",
"txHash": "0x…",
"explorerUrl": "https://basescan.org/tx/0x…",
"seal": "seal_…",
"amount": "2.5",
"amountUsd": "2.5",
"asset": "USDC",
"chainId": 8453,
"mode": "LIVE",
"checks": [ { "code": "AGENT_ACTIVE", "label": "Agent active", "passed": true }, … ]
}Concurrent requests for the same agent are serialised with a database lock, so two payments cannot both slip under a daily limit. A blocked request is stored with its checks and never signed.
POST /api/v1/payment-intents
Ask whether a payment would be approved before doing expensive work. Same payload as a payment; nothing is signed, reserved or broadcast.
{
"id": "pi_…",
"object": "payment_intent",
"decision": "BLOCKED",
"reason": "PER_TRANSACTION_LIMIT",
"checks": [ … ],
"expiresAt": "…"
}Read endpoints
GET /api/v1/payments?status=&agentId=&chainId=&limit=&cursor=— newest first, cursor pagination.GET /api/v1/payments/:id— full receipt.GET /api/v1/agents,GET /api/v1/agents/:id— agent, wallet address, balances (labelledON_CHAIN/SIMULATION) and spend.GET /api/v1/seals,GET /api/v1/seals/:id— the full policy.GET /api/health— mode, chains, database reachability, configuration flags (no auth).
Policy checks
Evaluated in order on every request; all are reported, the first failure becomes reason. Empty allow-lists allow nothing; an amount that cannot be valued in USD under a USD limit is blocked (fail closed).
| Code | Meaning |
|---|---|
| AGENT_ACTIVE | Agent active |
| SEAL_ATTACHED | Seal attached |
| SEAL_ACTIVE | Seal active |
| SEAL_EXPIRY | Seal not expired |
| EMERGENCY_FREEZE | Emergency freeze not engaged |
| SESSION_PERMISSION | Session permission active |
| NETWORK_ALLOWED | Network allowed |
| ASSET_ALLOWED | Asset allowed |
| RECIPIENT_ALLOWED | Recipient verified |
| CONTRACT_ALLOWED | Contract allowed |
| PRICE_AVAILABLE | USD value determined |
| PER_TRANSACTION_LIMIT | Under transaction limit |
| DAILY_LIMIT | Under daily limit |
| MONTHLY_LIMIT | Under monthly limit |
| GAS_LIMIT | Under gas limit |
TypeScript SDK
The SDK in packages/sdk is a thin, dependency-free client over the API.
import { LenlyPay } from "@lenlypay/sdk";
const lenly = new LenlyPay({ apiKey: process.env.LENLYPAY_API_KEY!, baseUrl: "https://your-deployment" });
const intent = await lenly.paymentIntents.create({ agentId, recipient, amount: "1.50", asset: "USDC" });
if (intent.decision === "APPROVED") {
const payment = await lenly.payments.create({ agentId, recipient, amount: "1.50", asset: "USDC" });
}
await lenly.agents.get(agentId);
await lenly.payments.list({ status: "BLOCKED" });
await lenly.seals.get(sealId);Live vs simulation
PAYMENT_MODE=live: approved payments are signed by the agent's delegated signer, broadcast through the configured RPC and confirmed; funding is a real transfer from your wallet that the server verifies on-chain.
PAYMENT_MODE=simulation: for local development only. Approved payments settle against a database ledger, carry mode: "SIMULATION" and a sim_… reference instead of a transaction hash, and the dashboard shows a SIMULATION MODE banner. They are never presented as on-chain payments.
Security model
- Signer keys are generated server-side and stored only as AES-256-GCM ciphertext bound to the wallet id; they are decrypted in memory to sign an already-approved transfer.
- The human wallet is never delegated; it only signs a sign-in statement and funding transfers it initiates itself.
- Every payment parameter is re-validated on the server; client-supplied amounts on funding are ignored in favour of the on-chain receipt.
- Freezing an agent, disabling or revoking a Seal, or rotating a key takes effect on the next request.
- Sessions are httpOnly JWT cookies; server actions are origin-checked; internal routes reject cross-site requests; the public API uses Bearer keys only.
- Every sensitive action is written to an append-only audit log.
Terms & privacy
LenlyPay stores account identifiers, agent metadata, policies, payment receipts and audit events needed to operate the service. Signer secrets are encrypted. No analytics or third-party trackers are embedded. On-chain transactions are public by nature. This software is provided as-is; operators are responsible for the funds they place in agent wallets and the Seals they configure.