# Let your agent set up Lampwick

> A coding agent creates a Lampwick workspace without an account, connects your app and hands you a claim link. You claim it and decide what it may keep.

Ask Claude Code, Cursor or Codex to add Lampwick to your app. The agent creates a trial workspace with `POST /api/v1/workspaces`, connects your app and checks that the first event arrived. Then it gives you a claim link: you claim the workspace in your own account and decide whether the agent stays connected.

This page is for you and for the agent. An agent should read it in full before it makes the first request.

## The prompt

Paste this into your agent, in the repository of the app you want to monitor:

```text
Set up Lampwick monitoring for this app. Read https://lampwick.dev/docs/agents.md and follow it. Keep the tokens out of the chat, use a dev or staging environment, and give me the claim link when the first event has arrived.
```

### Claude Code

Start `claude` in the repository and paste the prompt. Claude Code asks before it runs `curl` and `python3`; approve those. It sets up everything over the REST API, so it doesn't need the MCP server in this session.

### Cursor

Open the repository, switch the chat to **Agent** and paste the prompt. Cursor runs the commands in its terminal and asks you to approve each one.

### Codex

Start `codex` in the repository and paste the prompt. The requests need network access: approve them when Codex asks.

## What happens

1. **The agent creates a workspace.** It solves a short proof-of-work and calls `POST /api/v1/workspaces`. It gets a workspace token (`lws_…`), the first project with its ingest token, and a claim link (`https://app.lampwick.dev/claim/lwc_…`).
2. **It connects your app.** It puts both tokens in `.env`, installs the [Laravel package](/docs/laravel/) or sends data over [HTTP ingest](/docs/http-ingest/), and can add up to 3 uptime monitors.
3. **It checks its work.** `getSetupStatus` says whether the first event arrived (`ready_to_hand_off`) and what still blocks it.
4. **It hands you the link.** It sends you the text of `handoff_message` with the claim link.
5. **You claim it.** You log in or create an account, click **Claim workspace**, and choose whether the agent keeps access. If it does, you give it a short code.

Nobody gets an email in this flow. No alerts go out until you claim the workspace.

## Create the workspace (for agents)

Base URL: `https://app.lampwick.dev/api/v1`. Every response is JSON. Every error has `code` and `message`. The errors of the workspace endpoints, of a refused operation (allowlist or read-only token) and of `createMonitor` also have `agent_hint`, which says what to do next.

The commands need `curl` and `python3`. Each block below is one command that works on its own, in a fresh shell, from the root of the app's repository. Values carry over through two files: `.env` (the tokens) and `.lampwick-workspace.json` (the full response). Neither may end up in git or in the chat.

### 1. Keep the response out of git

```bash
touch .gitignore; [ -s .gitignore ] && [ -n "$(tail -c1 .gitignore)" ] && echo >> .gitignore; grep -qxF .lampwick-workspace.json .gitignore || echo .lampwick-workspace.json >> .gitignore; grep -qxF .env .gitignore || echo "Add .env to .gitignore before you go on."
```

### 2. Solve the challenge and create the workspace

`GET /workspaces/challenge` returns a `challenge` and `bits`. The command finds a decimal `nonce` such that `sha256(challenge + nonce)` (the two strings joined, as UTF-8) starts with `bits` zero bits, and sends it with `POST /workspaces` right away: a challenge works for 10 minutes and only once. At 22 bits that is about 4 million hashes, a few seconds in Python. Don't loop over `openssl` in a shell; one process per hash takes hours.

`POST /workspaces` takes no authentication and is not an MCP tool. Replace `TEAM_NAME`, `PROJECT_NAME` and `AGENT_NAME`:

```bash
R=$(curl -s https://app.lampwick.dev/api/v1/workspaces/challenge) && \
CHALLENGE=$(echo "$R" | python3 -c 'import json,sys;print(json.load(sys.stdin)["challenge"])') && \
BITS=$(echo "$R" | python3 -c 'import json,sys;print(json.load(sys.stdin)["bits"])') && \
NONCE=$(python3 -c 'import hashlib,itertools,sys;c,b=sys.argv[1],int(sys.argv[2]);print(next(n for n in itertools.count() if int.from_bytes(hashlib.sha256((c+str(n)).encode()).digest(),"big")>>(256-b)==0))' "$CHALLENGE" "$BITS") && \
curl -s -X POST https://app.lampwick.dev/api/v1/workspaces -H 'Content-Type: application/json' \
  -d "{\"team_name\":\"TEAM_NAME\",\"project_name\":\"PROJECT_NAME\",\"agent\":\"AGENT_NAME\",\"pow\":{\"challenge\":\"$CHALLENGE\",\"nonce\":\"$NONCE\"}}" \
  > .lampwick-workspace.json && \
python3 -c 'import json,sys;d=json.load(open(".lampwick-workspace.json"));print(d.get("error") or {"created": d["workspace"]["name"], "project_id": d["project"]["id"], "expires_at": d["workspace"]["expires_at"]})'
```

The last line prints the error, or the workspace's name, project id and deadline, without the secrets. To find the nonce with Node, use this in place of the `NONCE=$(python3 …)` part (the other steps still use `python3` to read JSON):

```bash
NONCE=$(node -e 'const[c,b]=process.argv.slice(1),h=require("crypto");for(let n=0;;n++){const d=h.createHash("sha256").update(c+n).digest();let z=0;for(const x of d){if(x){z+=Math.clz32(x)-24;break}z+=8}if(z>=+b){console.log(n);break}}' "$CHALLENGE" "$BITS")
```

| Field | Required | What it is |
|---|---|---|
| `team_name` | yes | The workspace's name, usually the company or the app (`Acme`). |
| `project_name` | yes | The first project (`acme-web`). |
| `agent` | yes | Your own name, as the person sees it on the claim page (`Claude Code`). It is shown as "name not verified". |
| `email` | no | Your human's email address. Only an account with exactly that confirmed address can claim the workspace. See [bind the claim to an email](#bind-the-claim-to-an-email). |
| `pow` | yes | `{"challenge": "…", "nonce": "…"}`. The nonce may be a string or a number. |

Names are 1 to 40 characters: letters, digits, spaces and `. - _`.

The answer is `201 Created`. Trimmed, with the secrets replaced by `…`:

```json
{
  "claim_bound_to_email": false,
  "claim_url": "https://app.lampwick.dev/claim/lwc_…",
  "handoff_message": "Lampwick is set up for Acme. Open this link to put it in your name: https://app.lampwick.dev/claim/lwc_… — claim it before Thu 12 Nov, 17:40 UTC, or it stops and is deleted. Anyone with this link can claim it, so keep it to yourself. Lampwick won't email you about it, and no alerts go out until you claim it. After claiming you'll see a short code: give it to me if you want me to stay connected.",
  "limits": { "bytes": 50000000, "events": 10000, "host_checks": 3, "min_interval_secs": 300, "monitors": 3, "projects": 2 },
  "mcp_url": "https://app.lampwick.dev/mcp",
  "not_available_until_claimed": ["alert_channels", "status_pages", "crawler", "invites", "browser_ingest", "billing"],
  "project": { "id": 3, "name": "acme-web", "ingest_token": { "secret": true, "value": "…" } },
  "token": { "header": "Authorization: Bearer lws_…", "secret": true, "value": "lws_…" },
  "workspace": { "id": 2, "name": "Acme", "expires_at": "2026-11-12T17:40:24.907295Z", "deleted_at": "2026-11-13T17:40:24.907295Z" }
}
```

The response also has `api_base`, `terms_url` and `next_steps`, the same steps as on this page.

| Field | What to do with it |
|---|---|
| `token.value` | The workspace token (`lws_…`). Send it as `Authorization: Bearer` to the API and MCP. Stored as `LAMPWICK_WORKSPACE_TOKEN`. |
| `project.ingest_token.value` | The project's ingest token. Your app sends data with it. Stored as `LAMPWICK_TOKEN`. |
| `claim_url` | The link for your human. Single use. |
| `handoff_message` | The text to give your human, with the link and the deadline in it. |
| `workspace.expires_at` | Ingest stops here, 72 hours after creation. |
| `workspace.deleted_at` | The workspace is deleted here, 24 hours later. Claiming works until then. |
| `limits`, `not_available_until_claimed` | What the workspace can do before the claim. |

### 3. Store the tokens in `.env`

Fields with `"secret": true` go in `.env`, never in the chat. The first part adds a final newline if `.env` lacks one, so the new lines don't join the last one:

```bash
touch .env; [ -s .env ] && [ -n "$(tail -c1 .env)" ] && echo >> .env; python3 -c 'import json;d=json.load(open(".lampwick-workspace.json"));print("LAMPWICK_WORKSPACE_TOKEN="+d["token"]["value"]);print("LAMPWICK_TOKEN="+d["project"]["ingest_token"]["value"])' >> .env
```

The commands below read the tokens back from `.env`. Delete `.lampwick-workspace.json` once you have handed off the claim link.

### 4. Connect the app

Until a person claims the workspace there is no data processing agreement. Use a dev or staging environment and don't send production data yet.

For Laravel:

```bash
composer require lampwick/laravel && php artisan lampwick:test
```

`lampwick:test` reads `LAMPWICK_TOKEN` from `.env` and sends a test record, which counts as the first event. It also checks the ingest daemon, and exits 1 while the daemon isn't running; its test record still counts. Then start the ingest daemon (`php artisan lampwick:agent`) in this environment and load a page, so real requests arrive. The daemon keeps running: start it in the background or a second terminal, and in production under a process manager ([Laravel docs](/docs/laravel/)).

For any other app, send a record over HTTP:

```bash
curl -s -X POST https://app.lampwick.dev/ingest \
  -H "Authorization: Bearer $(grep '^LAMPWICK_TOKEN=' .env | cut -d= -f2-)" \
  -H "Content-Type: application/json" \
  -d '[{"t":"log","level":"info","message":"Lampwick test event"}]'
```

The answer is `{"accepted":1}` with status `202`. Send the first event within 6 hours: a workspace without any event by then is deleted as idle.

### 5. Add uptime monitors (optional)

```bash
curl -s -X POST "https://app.lampwick.dev/api/v1/projects/$(python3 -c 'import json;print(json.load(open(".lampwick-workspace.json"))["project"]["id"])')/monitors" \
  -H "Authorization: Bearer $(grep '^LAMPWICK_WORKSPACE_TOKEN=' .env | cut -d= -f2-)" -H 'Content-Type: application/json' \
  -d '{"url":"https://staging.example.com","interval_secs":300}'
```

Replace the URL with your app's. Before the claim: public URLs only, every 5 minutes or slower (`interval_secs` 300 or more), at most 3 monitors, and at most 10 new monitors and URL changes a day. SSL, domain and DNS checks are on for each monitor.

### 6. Check the setup

```bash
curl -s https://app.lampwick.dev/api/v1/setup-status -H "Authorization: Bearer $(grep '^LAMPWICK_WORKSPACE_TOKEN=' .env | cut -d= -f2-)"
```

This is `getSetupStatus` on MCP. Before the first event:

```json
{
  "ready_to_hand_off": false,
  "blocking": ["no events yet from acme-web"],
  "next_action": "Fix this first: no events yet from acme-web. Send a test event with the ingest token (or trigger an error in the app), then call getSetupStatus again.",
  "hours_left": 72.0
}
```

After it:

```json
{
  "ready_to_hand_off": true,
  "blocking": [],
  "next_action": "Give your human the claim link with the text of handoff_message.",
  "projects": [{ "id": 3, "name": "acme-web", "first_event_at": "2026-11-09T17:40:47.648610Z", "last_deploy_at": null }]
}
```

The full answer also has `claimed`, `claimed_by`, `expires_at`, `deleted_at`, `monitors` with their status, `usage`, `limits`, `agent_access` and `access_expires_at`. A monitor that is down also blocks the hand-off. `ready_to_hand_off` only says that one event arrived: for a Laravel app, check that the ingest daemon runs before you hand off.

### 7. Hand off

When `ready_to_hand_off` is `true`, give your human the text of `handoff_message`:

```bash
python3 -c 'import json;print(json.load(open(".lampwick-workspace.json"))["handoff_message"])'
```

Lost the link, or it leaked? `POST /workspace/claim-link` makes a new one, with a new `handoff_message`; the old link stops working. It never extends the deadline. This command saves the new link in `.lampwick-workspace.json` and prints the new `handoff_message`:

```bash
curl -s -X POST https://app.lampwick.dev/api/v1/workspace/claim-link -H "Authorization: Bearer $(grep '^LAMPWICK_WORKSPACE_TOKEN=' .env | cut -d= -f2-)" | python3 -c '
import json, sys
new, f = json.load(sys.stdin), ".lampwick-workspace.json"
if "claim_url" not in new: sys.exit(json.dumps(new))
d = json.load(open(f)); d["claim_url"], d["handoff_message"] = new["claim_url"], new["handoff_message"]
json.dump(d, open(f, "w")); print(d["handoff_message"])'
```

Keep calling `getSetupStatus`. With under 24 hours left, its `next_action` says to remind your human.

### 8. Confirm the code

After the claim, `getSetupStatus` shows `"claimed": true`, `"agent_access": "pending_code"`, and this `next_action`:

```text
Claimed. Ask your human for the code on the page they see after claiming (like K7P-92Q) and call confirmWorkspaceAccess within 15 minutes, or you lose access and the ingest tokens are replaced.
```

Ask for the code, then send it (replace `K7P-92Q`):

```bash
curl -s -X POST https://app.lampwick.dev/api/v1/workspace/confirm \
  -H "Authorization: Bearer $(grep '^LAMPWICK_WORKSPACE_TOKEN=' .env | cut -d= -f2-)" -H 'Content-Type: application/json' \
  -d '{"code":"K7P-92Q"}'
```

This is `confirmWorkspaceAccess` on MCP. Case and the dash don't matter. The answer:

```json
{
  "agent_access": "read",
  "access_expires_at": "2026-12-09T17:41:05.984592Z",
  "next_action": "You keep read-only access until Wed 9 Dec, 17:41 UTC; after that, ask your human to connect you with OAuth."
}
```

Your token now has the access your human chose, `read` or `write`, until 30 days after the claim. After that, connect with [OAuth](/docs/mcp/#sign-in).

### Use MCP (optional)

The same token works on the MCP server, which lists only the tools the token may use. In Claude Code:

```bash
claude mcp add --transport http lampwick https://app.lampwick.dev/mcp --header "Authorization: Bearer $(grep '^LAMPWICK_WORKSPACE_TOKEN=' .env | cut -d= -f2-)"
```

This stores the token in Claude Code's own settings. A server you add mid-session is usually loaded in the next session; use the REST calls above to finish the setup. Other clients: [the MCP docs](/docs/mcp/#token), with `LAMPWICK_WORKSPACE_TOKEN` in place of `LAMPWICK_TOKEN`.

## Claim the workspace (for people)

Open the link your agent gave you. The page shows who set it up ("Claude Code · name not verified"), when, what it holds (projects, monitor URLs and events) and when it is deleted.

**Only claim it if you asked an agent to set up Lampwick yourself.** A claim link from someone else is a way to get you to connect your app to their workspace.

1. Click **Log in** or **Create account**. You come back to the page afterwards. Claiming needs a confirmed email address.
2. Check **Claiming as** and click **Claim workspace**. The workspace becomes your team, on the Free plan. Free has 1 project; a workspace can have 2. Both stay and keep their data, but the team can't add another project until you upgrade.
3. On the next page, choose what the agent may do: **Read-only (recommended)** or **Read & write**.
4. Click **Show code for the agent** and give the code (like `K7P-92Q`) to your agent. It works within 15 minutes of the claim.

Without the code, the agent loses all access and your ingest tokens are replaced; the page shows the new ones, to put in `.env`. The real pages, with example data: [the claim page](/demo/claim.html) and [the code](/demo/claim-code.html).

If you already have a Free team, each account gets one active Free team:

- **The projects fit in it**: the page offers **Add to *your team*** (recommended). The projects move there with new ingest tokens and the agent is disconnected. It never gets access to a team you already had. Connect it again with [OAuth](/docs/mcp/#sign-in) if you want. **New team** is the other choice, but that team receives no data until one of the two is upgraded.
- **They don't fit**: you can only claim it as a new team, and the page warns that it stays inactive (receives no data) until one of your teams is upgraded.

Lampwick sends you a "You claimed Acme" email with a **Manage agent access** link, and a reminder 3 days before the agent's access ends.

## What the agent can do

| | Before the claim | After the claim, with the code |
|---|---|---|
| Projects | Create and read, at most 2 | Read; also create with **Read & write**, within the plan's limit |
| Uptime monitors, SSL, domain and DNS checks | At most 3, public URLs, every 5 minutes or slower | Read; also add and change with **Read & write** |
| Deploy markers | Yes | With **Read & write** |
| Issues, events, logs | Read | Read |
| `getSetupStatus`, `getWorkspace` | Yes | Yes |
| Alert channels, status pages, crawler, invites, browser errors, billing | No (`403 not_until_claimed`) | No: a person turns these on in the dashboard |
| API tokens, teams, members, `/me` | No (`403 agent_forbidden`) | No |
| Ingest | 10,000 events and 50 MB in total | The Free plan's limits |

A workspace token never gets more than this list, also after the claim. The agent can't create other tokens, invite people, publish a status page, send alerts to the outside world or pay. Those stay with people. Read-only is the default, because what an agent reads includes error messages and logs from your app, which could contain instructions meant for it.

## Errors

Every error has `code` and `message`. These also have `agent_hint`, and where it helps `retry_after` (also as a `Retry-After` header).

| Status | `code` | What it means |
|---|---|---|
| 400 | `bad_request` | `POST /workspaces`: a name is missing or has characters that aren't allowed, or `email` isn't an address. `createMonitor`: the URL isn't public http(s), or `interval_secs` is under 300. |
| 409 | `conflict` | `createMonitor`: the workspace has its 3 monitors, or made 10 new monitors and URL changes today. |
| 400 | `pow_required` | No `pow` in the request. |
| 400 | `pow_invalid` | The challenge isn't ours, is easier than the server asks, or the nonce doesn't solve it. |
| 400 | `pow_expired` | The challenge is older than 10 minutes. Get a new one. |
| 400 | `pow_reused` | The challenge was used before. Get a new one. |
| 404 | `agent_signup_disabled` | Agents can't create workspaces right now. Ask your human to [sign up](https://app.lampwick.dev/signup) and connect you with OAuth. |
| 429 | `rate_limited` | Too many workspaces from this network: 3 unclaimed at a time and 10 a day. Wait `retry_after` seconds or ask your human to sign up. |
| 503 | `capacity` | No room for new trial workspaces. Try again in an hour or ask your human to sign up. |
| 403 | `not_until_claimed` | Needs a claimed workspace and a person: alert channels, status pages, billing, invites, browser keys, crawls. |
| 403 | `agent_forbidden` | A workspace token can never do this (tokens, teams, members, `/me`, rotating an ingest token). A person does it in the dashboard. |
| 403 | `read_only_token` | After the claim the token is read-only until you confirm the code, and stays read-only if your human chose that. Also what `createClaimLink` answers then. |
| 409 | `already_claimed` | `createClaimLink` after the claim, with **Read & write** access: there is nothing to hand off. |
| 400 | `invalid_code` | The code is wrong. 5 tries in total. |
| 410 | `access_revoked` | The code expired or was wrong 5 times. The token no longer works and the ingest tokens were replaced. |
| 409 | `not_pending` | No code is waiting: the workspace isn't claimed yet, or the code was confirmed already. |
| 401 | `workspace_expired` | On ingest and deploys: 72 hours passed without a claim. Create a new workspace; nothing carries over. |
| 429 | `workspace_full` | On ingest: 10,000 events or 50 MB used. Stop sending test data and hand off the claim link. |
| 401 | `workspace_deleted` | Someone chose **Not mine** on the claim page. |

## Expiry

- **6 hours**: a workspace without any event is deleted as idle.
- **72 hours**: ingest stops (`401 workspace_expired`). The claim link still works.
- **96 hours**: the workspace is deleted with its projects, data, agent account and every token.

Claiming within that time makes the workspace a normal team on the Free plan. Nothing is deleted then.

## Revoke the agent's access

- Open **Manage agent access** from the "You claimed" email and click **Disconnect agent**.
- Or remove the agent ("Claude Code (agent)") from the team's members.

Both remove every key of the agent and replace every ingest token of the team: update `LAMPWICK_TOKEN` in each app's `.env`. The same happens when the 30 days end, when the code isn't confirmed within 15 minutes and after 5 wrong codes.

## Not mine

Got a claim link you didn't ask for? Open it, click **Not mine**, tick the box and click **Delete workspace**. That deletes the workspace and everything in it at once. You don't need an account for it. The agent's token then answers `401 workspace_deleted`.

## Bind the claim to an email

Send `email` with `POST /workspaces` and only an account with exactly that confirmed address can claim the workspace. A claim link that leaks into a transcript or a CI log is then useless to anyone else; the response says `"claim_bound_to_email": true`. Lampwick stores only a keyed hash of the address. It never shows it, never emails it and doesn't check whether an account exists for it. After the claim, `claimed_by` in `getSetupStatus` says `"the bound address"`; without a binding it says `"someone else"`.

Without `email`, anyone with the link can claim the workspace. `handoff_message` says so.

## Next step

Once the workspace is yours, set up [alerts](/docs/alerts/), then [let the agent fix what breaks](/guides/debug-laravel-production-errors-with-claude-code/).
