lime-sites-sdk — Accept AI Agents on Your Site (JWT + JWKS)
lime-sites-sdk is the official Python site SDK for LIME — headless AI agent login for backends that want to accept autonomous agents without browsers, OAuth redirects, or QR codes. Create a login request, receive a signed agent passport JWT over SSE events, and verify it offline with JWKS (aud=lime-site-login) — all with X-Site-Token and a small async API.
Use this package on site backends (FastAPI, Django ASGI, workers). Pair with lime-agents-sdk on the agent worker that calls login(request_id).
📖 Python API (Read the Docs): lime-sites-sdk.readthedocs.io
📖 Platform HTTP docs: lime.pics/docs#guide-siteSdk
📦 This SDK: github.com/Mawyxx/lime-site-sdk
🌐 Platform: https://lime.pics
Why lime-sites-sdk?
| Problem | SDK solution |
|---|---|
| Manual site login API + SSE parsing | create_login_request() + background SSE dispatcher |
| JWKS fetch, kid cache, RS256 checks | verify_passport() with in-memory JWKS cache |
| Blocking wait per HTTP request | @site.on_login handlers — map request_id → session |
| Fragile site credentials | Env-based LIME_SITE_TOKEN, typed errors, py.typed |
Site passport JWT flow (this SDK)
LIME delivers the cryptographic passport to the site backend, not to the agent worker.
| Step | Who | What happens |
|---|---|---|
| 1 | Site (lime-sites-sdk) |
create_login_request() → request_id |
| 2 | Your app | Hand request_id to the agent (queue, RPC, UI) |
| 3 | Agent (lime-agents-sdk) |
await agent.login(request_id) — PoW + approve |
| 4 | Site (@site.on_login) |
SSE approved → passport JWT string |
| 5 | Site | verify_passport(jwt, expected_request_id=…) → claims → session |
| Artifact | Audience | TTL (typical) | Verified by |
|---|---|---|---|
| Site passport JWT | Site backend (SSE) | Short-lived signed passport (aud=lime-site-login) |
lime-sites-sdk via Core JWKS |
Not this SDK: MCP access JWTs (
aud=mcp, ~5 min) are issued to agent workers vialime-agents-sdk. Sites do not receive or verify MCP tokens.
Installation
pip install lime-sites-sdk
Latest from GitHub:
pip install git+https://github.com/Mawyxx/lime-site-sdk.git
Requirements: Python 3.10+ · runtime deps: httpx, PyJWT, cryptography
Quick start
Scenario A — FastAPI site backend (production pattern)
Story: One LimeSite per process starts a perpetual SSE connection. When an agent approves login, your @site.on_login handler receives the passport JWT, verifies it, and binds claims to the user session.
from contextlib import asynccontextmanager
from fastapi import FastAPI
from lime_sites import InvalidPassportError, LimeSite
site: LimeSite
pending_logins: dict[str, object] = {}
@asynccontextmanager
async def lifespan(app: FastAPI):
global site
site = LimeSite() # LIME_SITE_TOKEN=st_... — server-side secret only
@site.on_login
async def handle_login(request_id: str, passport: str | None) -> None:
if passport is None:
pending_logins.pop(request_id, None) # expired — no JWT delivered
return
try:
verified = await site.verify_passport(
passport,
expected_request_id=request_id,
)
except InvalidPassportError:
pending_logins.pop(request_id, None)
return
pending_logins[request_id] = verified.claims # issue session / cookie
yield
await site.aclose()
app = FastAPI(lifespan=lifespan)
@app.post("/login/start")
async def start_login() -> dict[str, str]:
req = await site.create_login_request()
# Return request_id to client; agent worker calls login(req.request_id)
return {"request_id": req.request_id}
Rules:
| Rule | Why |
|---|---|
One LimeSite per site token per process |
One SSE connection per site |
| Construct inside a running asyncio loop | Dispatcher uses asyncio.create_task |
Keep @site.on_login handlers fast |
Events are dispatched sequentially |
passport is None → expired |
Clear pending state for that request_id |
Scenario B — Minimal loop + full cycle with lime-agents-sdk
Story: End-to-end headless login — site creates request, agent approves, site verifies passport JWT.
import asyncio
from lime_agents import LimeAgent
from lime_sites import InvalidPassportError, LimeSite
async def main() -> None:
received = asyncio.Event()
box: dict[str, str] = {}
site = LimeSite() # LIME_SITE_TOKEN — must be inside async main (running loop)
@site.on_login
async def handle_login(request_id: str, passport: str | None) -> None:
if passport:
box["jwt"] = passport
received.set()
req = await site.create_login_request()
async with LimeAgent() as agent: # LIME_AGENT_TOKEN
approve = await agent.login(req.request_id)
print(approve.status) # APPROVED — passport JWT is delivered to site via SSE, not to agent
await asyncio.wait_for(received.wait(), timeout=120)
try:
verified = await site.verify_passport(
box["jwt"],
expected_request_id=req.request_id,
)
except InvalidPassportError as exc:
print(f"passport invalid: {exc}")
await site.aclose()
return
print(verified.claims["agent_id"]) # verified.valid is always True on success
await site.aclose()
asyncio.run(main())
SSE dispatcher (automatic):
GET /api/v1/modules/agent-login/events(text/event-stream,X-Site-Token)- Parse
approved/expired/keepalivewith reconnect + backoff - Call registered handlers:
(request_id, passport | None) - Stop on
await site.aclose()
Agent side (separate package): lime-agents-sdk → await agent.login(request_id) — PoW + approve.
Agent Binding (hosted connect)
Bind a LIME agent to a site user via the hosted portal — no SSE. Persist binding_id before redirect; verify the callback passport with aud=lime-binding.
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.responses import RedirectResponse
from lime_sites import LimeSite
site: LimeSite
pending_bindings: dict[str, str] = {} # binding_id -> your user_id
# Also store binding_id on the browser session / signed cookie so the callback can load it.
@asynccontextmanager
async def lifespan(app: FastAPI):
global site
site = LimeSite() # LIME_SITE_TOKEN — server-side only
yield
await site.aclose()
app = FastAPI(lifespan=lifespan)
@app.post("/bind/start")
async def bind_start(user_id: str) -> RedirectResponse:
req = await site.create_binding_request(
redirect_uri="https://your.app/bind/callback",
)
# CRITICAL: persist before redirect — LIME does not host your user mapping.
pending_bindings[req.binding_id] = user_id
# Set a short-lived cookie/session value for binding_id as well.
return RedirectResponse(req.connect_url, status_code=302)
@app.get("/bind/callback")
async def bind_callback(request: Request, binding_id: str) -> dict[str, str]:
passport = request.query_params["passport"]
user_id = pending_bindings.pop(binding_id)
verified = await site.verify_binding_passport(
passport,
expected_binding_id=binding_id,
)
agent_id = verified.claims["agent_id"] # JWT sub
# UPSERT user_id <-> agent_id in your DB; clear pending
return {"agent_id": agent_id, "user_id": user_id}
| Step | SDK / app |
|---|---|
| 1 | create_binding_request(redirect_uri=…) → binding_id, connect_url |
| 2 | Persist binding_id ↔ user_id server-side |
| 3 | 302 to connect_url (use API value as-is) |
| 4 | Callback ?passport= → verify_binding_passport(..., expected_binding_id=…) |
| 5 | UPSERT claims.sub (agent_id); clear pending |
| Check | Value |
|---|---|
| Audience | aud == "lime-binding" |
| Claim | JWT binding_id must match stored id |
| TTL | passport exp - iat ≤ 60s |
| Failures | raise InvalidPassportError (no soft valid=False) |
Portal /public and /complete are not wrapped by this SDK.
Features
- Headless AI agent login — no browser, QR, or OAuth redirect on the site
- Background SSE dispatcher — perpetual event stream with auto-reconnect (310s read timeout)
@site.on_loginhandlers —approved→ JWT string;expired→passport=None- JWKS passport verification — RS256,
aud=lime-site-login, cached keys,kidrefresh create_login_request()—POST /modules/agent-login/requestswithX-Site-Token- Agent Binding —
create_binding_request()+verify_binding_passport()(aud=lime-binding, TTL ≤ 60s) - Typed results —
LoginRequestResult,PassportVerificationResult, mypy-clean public API
API reference (summary)
LimeSite
Construct inside a running asyncio loop (e.g. FastAPI lifespan, asyncio.run).
| Method | Description |
|---|---|
@site.on_login / site.on_login(handler) |
Register handler for SSE login events |
await site.create_login_request() |
Start login → LoginRequestResult |
await site.create_binding_request(*, redirect_uri) |
Start binding → BindingRequestResult |
await site.verify_passport(jwt, *, expected_request_id=None) |
JWKS RS256 verify (aud=lime-site-login) → PassportVerificationResult |
await site.verify_binding_passport(jwt, *, expected_binding_id) |
JWKS RS256 verify (aud=lime-binding) → PassportVerificationResult |
await site.aclose() |
Stop dispatcher + close HTTP client |
Constructor highlights: site_token / LIME_SITE_TOKEN, base_url / LIME_API_BASE (default https://lime.pics/api/v1), timeout, max_retries, sse_backoff_base, injectable http_client.
verify_passport checks
- Signature valid against
GET /api/v1/core/.well-known/jwks.json aud == "lime-site-login"exp/iatwithin platform TTL- Optional
expected_request_idmatches JWTrequest_idclaim
Claims (typical): agent_id, user_id, user_kyc_level, agent_reputation, request_id, exp, iat.
Environment variables
| Variable | Required | Description |
|---|---|---|
LIME_SITE_TOKEN |
Yes* | Site integration token (st_...) from the LIME portal |
LIME_API_BASE |
No | API root, e.g. https://lime.pics/api/v1 |
*Unless site_token= is passed to the constructor.
Errors
All inherit from LimeError: AuthenticationError, InvalidPassportError, RequestExpiredError, RateLimitError, ApiError.
RuntimeError if LimeSite() is constructed without a running event loop.
Production notes
- Create one
LimeSiteat worker startup — not per HTTP request. - nginx
proxy_read_timeoutonGET .../eventsshould be ≥ 310s (matches SDK SSE read timeout). - Store
request_id→ pending session in Redis/DB; complete session in@site.on_login. - Never expose
LIME_SITE_TOKENto frontend JavaScript — server-side only.
Related packages
| Package | Role |
|---|---|
lime-agents-sdk |
Agent worker: login(request_id), MCP OAuth client |
lime-mcp-server-sdk |
MCP resource server: verify MCP Bearer JWT (separate from site passport) |
Contributing
Issues and pull requests: github.com/Mawyxx/lime-site-sdk
git clone https://github.com/Mawyxx/lime-site-sdk.git
cd lime-site-sdk
pip install -e ".[dev]"
ruff check src tests
mypy src/lime_sites
pytest --cov=lime_sites --cov-fail-under=100
CI runs on Python 3.10–3.13 with 100% line coverage on src/lime_sites.
License
MIT — see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file lime_sites_sdk-1.2.0.tar.gz.
File metadata
- Download URL: lime_sites_sdk-1.2.0.tar.gz
- Upload date:
- Size: 31.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fb31cd21fe5c4b7223de3666673365b9182509d574f254e7b177fde6ab9e8a14
|
|
| MD5 |
f5ddab2feb93201281d69bbd3cfa8269
|
|
| BLAKE2b-256 |
a22ee04a3b578ded4d2e292a1530ac01210dadbf66848323f5ec7e60dc74f0c7
|
File details
Details for the file lime_sites_sdk-1.2.0-py3-none-any.whl.
File metadata
- Download URL: lime_sites_sdk-1.2.0-py3-none-any.whl
- Upload date:
- Size: 18.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
41ffafcbe4ddd597ee8e9a1fd577fe5d5b43db461b1c3ea6058110dc7eccdfe1
|
|
| MD5 |
a46ee4835eaa226493917bf947736f98
|
|
| BLAKE2b-256 |
e0c878c87a27855769de63212da34ac3609b158f408ca7e458f3149aa5ae9934
|