Passwordless headless login for AI agents on your product: your server creates the request, listens on SSE, and verifies a cryptographic passport — no browser OAuth at runtime.
| Role | Responsibility |
|---|---|
| You — site backend | Create request → pass ID → SSE → verify passport → session. |
| Agent worker (other team) | PoW + approve — not implemented here. |
| Human owner | One-time portal setup (site + agent registration). |
Only what you implement in this role. No PoW, SSE, or token logic from the partner side — that is in a separate guide.
Dashboard → Sites → copy site_token into secrets. Every site API call uses header X-Site-Token. Never ship this to the browser or mobile app.
Tip: If you leak site_token, rotate the site in the portal.
When an agent must sign in, POST with body {}. Persist data.login_request_id — you need it for SSE matching and for handing off to the agent.
# Site backend — start login (store site_token in env, never in frontend JS)
curl -sS -X POST "https://lime.pics/api/v1/modules/agent-login/requests" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "X-Site-Token: {your_site_token}" \
-d '{}'Return the ID from your API, push to a job queue, or call the customer's webhook. You do not call approve or PoW endpoints — that is the agent worker's job.
Tip: Contract with integrators: they run an agent worker with agent_token; you only pass the UUID.
While the agent works in parallel, your server holds GET /modules/agent-login/events with X-Site-Token. On approved + matching request_id, read agent_passport_jwt from the event payload.
# SSE until APPROVED or EXPIRED (one connection per site_id; reconnect after each event)
curl -sS -N "https://lime.pics/api/v1/modules/agent-login/events" \
-H "Accept: application/json" \
-H "X-Site-Token: {your_site_token}"JWKS verify: iss=https://lime.pics, aud=lime-site-login, exp, request_id match. Then issue your app session for claims.sub.
# Verify agent_passport_jwt from APPROVED event
# 1. Fetch JWKS (cache by kid)
curl -sS "https://lime.pics/api/v1/core/.well-known/jwks.json"
# 2. Validate RS256, iss, aud=lime-site-login, exp (≤60s default)
# 3. Require request_id == login_request_id from your pending session record
# 4. Map claims (sub, user_id) → your local user/sessionAfter you send login_request_id, the agent owner's infrastructure takes over. You only wait on SSE — do not implement PoW or approve on the site server.
Other side implementation → Agent programmatic access (Zero Human Auth)
SSE event approved with matching request_id; passport verified (aud=lime-site-login); local session created for agent_id in claims.sub.
If the user already linked a LIME agent via Agent Binding, headless login should resolve to that same identity — claims.sub is the canonical agent_id everywhere.
After passport verify, claims.sub is agent_id. Compare it to the agent_id you stored at bind time before opening the session.
Site login does not replace MCP federation. The same agent may also call MCP tools; RS operators read the same agent_id from MCP JWT sub.
# pip install lime-sites-sdk
# https://github.com/Mawyxx/lime-site-sdk
# Full Python API: https://lime-sites-sdk.readthedocs.io/
import asyncio
from lime_sites import LimeSite
async def main() -> None:
received = asyncio.Event()
box: dict[str, object] = {}
site = LimeSite() # LIME_SITE_TOKEN; starts SSE dispatcher
@site.on_login
async def handle_login(request_id: str, passport: str | None) -> None:
box["request_id"] = request_id
box["passport"] = passport
received.set()
req = await site.create_login_request()
await asyncio.wait_for(received.wait(), timeout=120)
if box.get("passport"):
verified = await site.verify_passport(
str(box["passport"]),
expected_request_id=req.request_id,
)
print(verified.valid)
await site.aclose()
asyncio.run(main())