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.1.tar.gz (31.7 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.1-py3-none-any.whl (18.2 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: lime_sites_sdk-1.2.1.tar.gz
  • Upload date:
  • Size: 31.7 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.1.tar.gz
Algorithm Hash digest
SHA256 23d3e320f9f0979f8210392327d145cf07cd70fd7eaf8db0eae1be1150425392
MD5 b37f974b15e2471715e6e6d886a9399e
BLAKE2b-256 f047a8b61d00c8f539bbf07ca3213411c94b411b7fc4d1ce61f35da17cdef4d6

See more details on using hashes here.

File details

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

File metadata

  • Download URL: lime_sites_sdk-1.2.1-py3-none-any.whl
  • Upload date:
  • Size: 18.2 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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 9fcdedabcd986936ae006f38761bc080db34736e76b5f2eda1c085e7e76d138d
MD5 ea7fd4bcc02ca4464f77eb67cc7a95f4
BLAKE2b-256 56c2244be120a74faabe2dbb225df8e5d8bff85eb89bf18a8425fb4c222134a1

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