For agent developers
Approve a site sign-in
This guide shows the agent side of a site sign-in in about 10 minutes: read the login_request_id from the waiting screen, solve the proof of work and approve. The site backend creates the request and receives the passport — that part is the site guide. For AI agents: /docs/all.en.md.
At a glance#
| Fact | Value |
|---|---|
| Effort | ~10 minutes |
| Type | Proof of work + approve call |
| Delivery | Agent runtime → LIME |
How it works#
- The agent opens your site in a browser and clicks Sign in with LIME; the waiting screen shows
login_request_id. - The agent reads the ID, solves the PoW and approves with its Agent Token.
- The site receives the passport over SSE; the agent holds no session.
What you need#
Register an agent in the LIME portal and keep LIME_AGENT_TOKEN in a .env file. The agent must belong to the same portal account as the site.
LIME_AGENT_TOKEN=<AGENT_TOKEN>Step 1. Read the ID from the waiting screen#
The agent drives the browser on your site and clicks Sign in with LIME; your page then exposes the login_request_id. The site must expose that id in a machine-readable form (a JSON field the agent can fetch, or parseable text in the waiting screen) — LIME does not push it to the agent, and an id shown only as an image is not enough.
Result: the agent holds the login_request_id (a UUID v4) for this browser attempt.
Step 2. Solve the PoW and approve#
The agent reads the challenge and difficulty from the public endpoint, solves the proof of work (PoW), and sends the approve call with its Agent Token. There is no redirect to lime.pics.
With the SDK, login(request_id) -> ApprovalResult does all three steps: it reads the public challenge, solves the PoW on a worker thread (bounded by pow_timeout, default 10 s), then approves. If the PoW cannot be solved within pow_timeout, it raises PowTimeoutError. The SDK does not retry the login — handle the error and the retry yourself.
import asyncio
from lime_agents import LimeAgent
agent = LimeAgent(agent_token="<AGENT_TOKEN>") # replace with your Agent Token
async def main() -> None:
await agent.login("<LOGIN_REQUEST_ID>")
await agent.aclose()
if __name__ == "__main__":
asyncio.run(main())The public read GET /api/v1/auth/requests/<LOGIN_REQUEST_ID> returns request_id, status, pow_challenge (a base64url string over 32 random bytes, 43 URL-safe characters), pow_difficulty (1–32, default 15), and expires_at, all inside the {"ok": true, "data": {...}} envelope. Hash the challenge text exactly as received — UTF-8, no base64 decode; the rule is proof of work. Manual, without the SDK — index the envelope to reach data:
import hashlib, requests
BASE = "https://lime.pics/api/v1"
rid = "<LOGIN_REQUEST_ID>"
payload = requests.get(f"{BASE}/auth/requests/{rid}").json()
data = payload["data"]
for n in range(1 << 32):
msg = b"lime:pow:v1\0" + data["pow_challenge"].encode() + b"\0" + str(n).encode()
if int.from_bytes(hashlib.sha256(msg).digest(), "big") >> (256 - data["pow_difficulty"]) == 0:
break
requests.post(f"{BASE}/modules/agent-login/requests/{rid}/approve",
json={"pow_nonce": str(n)}, headers={"X-Agent-Token": "<AGENT_TOKEN>"})Result: 200 with the request status — APPROVED; a replay from the same agent can return DELIVERING while the passport is in flight, and DELIVERED once it has reached the site; the first approver wins.
The public challenge read is one-shot: once an approve succeeds the stored challenge is consumed, so the same GET /api/v1/auth/requests/<LOGIN_REQUEST_ID> then returns 404 SITE_LOGIN_REQUEST_NOT_FOUND. The approve response is not the outcome — the site receives the passport over SSE, and that delivery is what finishes the flow.
Example response (JSON)#
The approve call returns the full request data:
{
"ok": true,
"data": {
"request_id": "<LOGIN_REQUEST_ID>",
"site_id": "<SITE_ID>",
"webhook_url": null,
"status": "APPROVED",
"created_at": "<CREATED_AT>",
"expires_at": "<EXPIRES_AT>",
"approved_agent_id": "<AGENT_ID>",
"approved_at": "<APPROVED_AT>",
"delivered_at": null,
"create_agent_id": null,
"create_idempotency_key": null
}
}The status may be DELIVERING on a replay race, or DELIVERED once the passport has reached the site. create_agent_id and create_idempotency_key are reserved and always null today.
Verify it worked#
| Check | Expected | Negative |
|---|---|---|
| Valid approve | 200 with status APPROVED; DELIVERING on a replay race, or DELIVERED after delivery | — |
Missing pow_nonce | the approve is sent with the solved nonce | 400 MISSING_POW_NONCE |
Wrong pow_nonce | re-solve against the latest challenge and retry | 400 INVALID_POW |
| Agent from another LIME account | the agent and site share one portal account | 404 SITE_LOGIN_REQUEST_NOT_FOUND |
| Approve after the TTL | the request is approved in time | 409 SITE_LOGIN_REQUEST_EXPIRED |
Duplicate approve (same agent, still APPROVED/DELIVERING) | 200 — idempotent re-delivery; stop retrying | 409 SITE_LOGIN_CONFLICT after DELIVERED, or from a different approved_agent_id |
| Approve response lost (timeout) | the site's SSE delivery is the outcome | — |
Codes and errors#
LIME returns these codes to the agent; the site never relays them — act on them directly. They arrive in the envelope {ok:false,error:{code,message,detail}}; the code column is error.code.
| Code | HTTP | Cause | Action |
|---|---|---|---|
MISSING_POW_NONCE | 400 | The approve body has no pow_nonce | Solve the challenge and send the nonce |
INVALID_POW | 400 | pow_nonce fails the difficulty check | Re-solve against the latest pow_challenge |
SITE_LOGIN_CAPABILITY_DENIED | 403 | The agent lacks the site_login:approve capability | Use an agent registered in the portal |
SITE_LOGIN_REQUEST_NOT_FOUND | 404 | Unknown id, or the agent and site are in different accounts | Check the id on the waiting screen and the portal account |
SITE_LOGIN_REQUEST_EXPIRED | 409 | The approve arrived after 120 seconds | Wait for a fresh id on the site |
SITE_LOGIN_CONFLICT | 409 | The request is already DELIVERED, or the caller is not the approved_agent_id | Stop retrying; a repeat from the same agent before delivery is idempotent (200) |