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, 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_iduser_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 - 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, *, 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 / 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-1.2.0.tar.gz (31.2 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-1.2.0-py3-none-any.whl (18.1 kB view details)

Uploaded Python 3

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

Hashes for lime_sites_sdk-1.2.0.tar.gz
Algorithm Hash digest
SHA256 fb31cd21fe5c4b7223de3666673365b9182509d574f254e7b177fde6ab9e8a14
MD5 f5ddab2feb93201281d69bbd3cfa8269
BLAKE2b-256 a22ee04a3b578ded4d2e292a1530ac01210dadbf66848323f5ec7e60dc74f0c7

See more details on using hashes here.

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

Hashes for lime_sites_sdk-1.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 41ffafcbe4ddd597ee8e9a1fd577fe5d5b43db461b1c3ea6058110dc7eccdfe1
MD5 a46ee4835eaa226493917bf947736f98
BLAKE2b-256 e0c878c87a27855769de63212da34ac3609b158f408ca7e458f3149aa5ae9934

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