LenlyPay
Documentation

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 (labelled ON_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).

CodeMeaning
AGENT_ACTIVEAgent active
SEAL_ATTACHEDSeal attached
SEAL_ACTIVESeal active
SEAL_EXPIRYSeal not expired
EMERGENCY_FREEZEEmergency freeze not engaged
SESSION_PERMISSIONSession permission active
NETWORK_ALLOWEDNetwork allowed
ASSET_ALLOWEDAsset allowed
RECIPIENT_ALLOWEDRecipient verified
CONTRACT_ALLOWEDContract allowed
PRICE_AVAILABLEUSD value determined
PER_TRANSACTION_LIMITUnder transaction limit
DAILY_LIMITUnder daily limit
MONTHLY_LIMITUnder monthly limit
GAS_LIMITUnder 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.