Skip to main content

lime-sites-sdk — Accept AI Agents on Your Site (JWT + JWKS)

lime-sites-sdk is the official Python site SDK for LIMEheadless 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).

PyPI version Python versions License: MIT CI Documentation MCP compatible

📖 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 approvedpassport 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 via lime-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 Noneexpired 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):

  1. GET /api/v1/modules/agent-login/events (text/event-stream, X-Site-Token)
  2. Parse approved / expired / keepalive with reconnect + backoff
  3. Call registered handlers: (request_id, passport | None)
  4. Stop on await site.aclose()

Agent side (separate package): lime-agents-sdkawait 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) -> dict[str, str]:
    passport = request.query_params["passport"]
    # Crypto only — signature, aud, TTL, non-empty binding_id claim.
    verified = await site.verify_binding_passport(passport)
    binding_id = verified.claims["binding_id"]
    # Business logic: load PENDING by claims.binding_id, enforce ownership.
    user_id = pending_bindings.pop(binding_id)
    agent_id = verified.claims["agent_id"]  # JWT sub
    # UPSERT user_id <-> agent_id in your DB
    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_iduser_id server-side
3 302 to connect_url (use API value as-is)
4 Callback ?passport=verify_binding_passport(jwt) (crypto only)
5 Load PENDING by claims["binding_id"]; UPSERT agent_id; clear pending
Check Value
Audience aud == "lime-binding"
Claim JWT must include non-empty binding_id (match to pending is app-owned)
TTL passport exp - iat60s
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_login handlersapproved → JWT string; expiredpassport=None
  • JWKS passport verification — RS256, aud=lime-site-login, cached keys, kid refresh
  • create_login_request()POST /modules/agent-login/requests with X-Site-Token
  • Agent Bindingcreate_binding_request() + verify_binding_passport() (aud=lime-binding, TTL ≤ 60s)
  • Typed resultsLoginRequestResult, 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) 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 / iat within platform TTL
  • Optional expected_request_id matches JWT request_id claim

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 LimeSite at worker startup — not per HTTP request.
  • nginx proxy_read_timeout on GET .../events should 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_TOKEN to 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

lime_sites_sdk-2.0.0.tar.gz (31.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

lime_sites_sdk-2.0.0-py3-none-any.whl (18.4 kB view details)

Uploaded Python 3

File details

Details for the file lime_sites_sdk-2.0.0.tar.gz.

File metadata

  • Download URL: lime_sites_sdk-2.0.0.tar.gz
  • Upload date:
  • Size: 31.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for lime_sites_sdk-2.0.0.tar.gz
Algorithm Hash digest
SHA256 de69dc9bc2589368cdb3d6431e3273714df5b4dd3021a4a894e1fc2166598f64
MD5 bed707dfd9f4ad49cb36241b5b5daa02
BLAKE2b-256 f66af69974a23b9c10363a5556599f7189b343f0de46f5f74863923add078512

See more details on using hashes here.

File details

Details for the file lime_sites_sdk-2.0.0-py3-none-any.whl.

File metadata

  • Download URL: lime_sites_sdk-2.0.0-py3-none-any.whl
  • Upload date:
  • Size: 18.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for lime_sites_sdk-2.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5cf1d5cd273a11cbbc979ddb1ef9c54443aeffd0a5d6cad6ea8c37c0e06fa38b
MD5 de55ed60b4cc47aef750fb3623421a73
BLAKE2b-256 eba954238e106e856dfd0c6a6776b77dbdc9bf40ef5fc516c015e14fed665c30

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page