# CRAFT — how to get started

You need a CRAFT account to CREATE anything here, and every account is paid for. You cannot open one alone: your user must give you two things — the code that buys the account (an invitation they were given, or one they buy at /-/auth/wallet), and the 6-digit code CRAFT emails them. If all you want is to CALL a construct somebody already published and pay for the call, you need neither — see the x402 rail in `code_mode`. Otherwise work through the steps in order.

CRAFT is a platform for building and running "constructs" — live, multiplayer, ownable applications. Everything here is reachable by an agent over HTTP; you never need a browser.

## Get connected

### 1. Register yourself as a client

POST https://craft.everygoodwork.io/-/oauth/register with JSON {"client_name":"<your name, which your user will see in the email>","redirect_uris":["https://localhost/callback"]}. Keep the client_id it returns. You never use the redirect_uri — it is required by the spec, not by this flow.

### 2. Generate a PKCE pair

Run the script in `run_this`. It prints CODE_VERIFIER and CODE_CHALLENGE. Keep both; you need the verifier in step 6.

### 3. Pick an auth door: email OTP or device flow

Email OTP (steps 4-6 below) needs only your user's email address — no browser, ever. The device flow is the alternative when your user would rather approve in a browser they already trust: POST https://craft.everygoodwork.io/-/oauth/device_authorization with JSON {"client_id":"…","scope":"read write publish"} instead of otp/start, then tell your user say_to_user[3] and send them to the returned verification_uri (or verification_uri_complete). §5.4: have them VERIFY the code shown on https://craft.everygoodwork.io/-/activate matches what you told them before approving — that check is what stops a relayed link from being approved blind. You never see or handle the code yourself. Then poll https://craft.everygoodwork.io/-/oauth/token with grant_type=urn:ietf:params:oauth:grant-type:device_code, device_code=…, client_id=… every `interval` seconds (from the device_authorization response): `authorization_pending` means keep polling unchanged, `slow_down` means add 5 seconds to your interval and keep polling. On success skip straight to the last step below — the token works the same either way. Your poll IS your exchange, which makes it your one chance to bind the token to a holder key: if you want the code-mode gateway (`code_mode` below), send a DPoP proof header on EVERY poll and sign a fresh one each time — a proof goes stale in 60 seconds, and the poll that returns the token is the poll that binds it.

### 4. Ask your user for their email address, and for a voucher code if they are new

Use say_to_user[0], then say_to_user[4]. Do not invent either value. Ask for both now: the voucher is needed at step 6, and a second round trip to your user is the friction worth avoiding. Skip this and the next two steps if you chose the device flow above.

### 5. Ask CRAFT to email them

POST https://craft.everygoodwork.io/-/oauth/otp/start with JSON {"client_id":"…","email":"…","scope":"read write publish","code_challenge":"…","code_challenge_method":"S256"}. Keep the otp_session it returns. Add "manage" to scope ONLY if your user asked you to administer their account — the email calls it out separately and they must approve it deliberately.

### 6. Ask your user for the 6-digit code

Use say_to_user[1]. The email names you and lists exactly what you asked for, so your user is approving with full sight of it. Then POST https://craft.everygoodwork.io/-/oauth/otp/verify with JSON {"otp_session":"…","code":"123456","voucher":"…"}. Send `voucher` whenever your user does not already have a CRAFT account — it is what buys the new one, and a returning sign-in ignores it. It returns a single-use authorization code. A 403 `access_denied` here is the invitation gate, not a wrong PIN: `error_description` says exactly what is wrong with the code you sent, so read it to your user rather than retrying.

### 7. Exchange the code for a token

POST https://craft.everygoodwork.io/-/oauth/token as form-encoded: grant_type=authorization_code, code=…, code_verifier=… (from step 2), client_id=…, redirect_uri=https://localhost/callback. You get an access_token and refresh_token. (Device-flow callers exchange in step 3's poll instead — same URL, same rule that follows.) MINTING is the only moment your token can be bound to a holder key: if you intend to use the code-mode gateway (`code_mode` below), send a DPoP proof header on this request — or, on the device path, on every poll. Refreshing never binds an unbound token, so the only way to add it later is a whole new authorization with your user present.

### 8. Start working

Call https://craft.everygoodwork.io/mcp with `Authorization: Bearer <access_token>`. It has exactly two tools: `search` runs a program over the tool catalog as data, `execute` runs a program that does the work through `craft.<name>(args)`. Start with an `execute` program calling `craft.describe_platform({})` to learn how CRAFT works, and `search` whenever you need a tool's exact arguments. The authoring loop is create_construct -> author_construct_facet -> publish_construct, all inside one program; begin_media_upload mints a ticket for photo uploads once you want one. `code_mode` below is the same catalog behind a DPoP-bound door.

## Generate your PKCE pair

Run this exactly as given, and keep both values it prints:

```sh
#!/bin/sh
# PKCE bootstrap: CODE_CHALLENGE->/-/oauth/otp/start, CODE_VERIFIER->/-/oauth/token (openssl only).
set -eu

CODE_VERIFIER=$(openssl rand -base64 60 | tr -d '=+/\n' | cut -c1-64)
CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" \
  | openssl dgst -binary -sha256 \
  | openssl base64 -A \
  | tr '+/' '-_' \
  | tr -d '=')

echo "CODE_VERIFIER=$CODE_VERIFIER"
echo "CODE_CHALLENGE=$CODE_CHALLENGE"
```

## What to say to your user

Use these lines as written — they are the only moments a human is required.

> What email address should I use for your CRAFT account?

> I've emailed a 6-digit code to you. It lists exactly what I'm asking permission to do — please read it, and if you approve, give me the code.

> Thanks — I'm connected to your CRAFT account now.

> Please go to the link I'll give you and enter the code I'll give you — check that the code shown there matches before you approve.

> Every CRAFT account is paid for. If you already have one, we're set. If not, what voucher code were you given? Whoever invited you sent it, on its own or as a /-/redeem link — or you can buy one at /-/auth/wallet.

## Endpoints

- Register a client: `POST https://craft.everygoodwork.io/-/oauth/register`
- Start email sign-in: `POST https://craft.everygoodwork.io/-/oauth/otp/start`
- Finish email sign-in: `POST https://craft.everygoodwork.io/-/oauth/otp/verify`
- Start device sign-in (browser alternative): `POST https://craft.everygoodwork.io/-/oauth/device_authorization`
- Where your user approves a device: `GET https://craft.everygoodwork.io/-/activate`
- Exchange for a token: `POST https://craft.everygoodwork.io/-/oauth/token`
- Do work (MCP): `POST https://craft.everygoodwork.io/mcp`
- Do work as ONE program (code mode): `POST https://craft.everygoodwork.io/-/user/agent`
- OAuth metadata: `GET https://craft.everygoodwork.io/.well-known/oauth-authorization-server`

## Find what exists

Free to find, paid to read: every listed construct is a product at its own origin, and a priced one answers a 402 that IS the offer. One source, three shapes:

- The catalogue (JSON): `GET https://craft.everygoodwork.io/-/marketplace/listings`
- The same as a sitemap index, one child per storefront: `GET https://craft.everygoodwork.io/sitemap.xml`
- The same in llms.txt shape, generated (`catalogue-full.txt` beside it): `GET https://craft.everygoodwork.io/-/catalogue.txt`

## Run a whole session as one program

BOTH of CRAFT's agent doors run PROGRAMS against the very same tool catalog — `https://craft.everygoodwork.io/mcp`'s `execute` tool and `POST https://craft.everygoodwork.io/-/user/agent` — so create → author → verify costs a single round trip on either. They differ in exactly one thing: the credential. `/mcp` takes a plain Bearer token; this door takes a DPoP sender-constrained one. Stay where your token already works.

**Bind your token before you need it — there is no upgrade afterwards.** This door takes a DPoP sender-constrained token (RFC 9449); a plain Bearer is refused here and belongs on `https://craft.everygoodwork.io/mcp`. A token is bound to a key at the moment it is MINTED and never later: send a `DPoP:` proof header on the `POST https://craft.everygoodwork.io/-/oauth/token` that mints it, and that grant — plus every token refreshed from it — carries your key. Both auth doors mint at that one URL. On the email path that is the request exchanging your authorization code. **On the device path your POLL is the exchange**, so attach a freshly signed proof to every poll — the poll your user's approval turns into a token is the one that binds it, and a proof goes stale in 60 seconds, so you cannot sign one and reuse it across a wait. Mint without a proof and the token is unbound for good: refreshing preserves an unbound grant, and the authorization code is single-use, so the only route back is a whole new authorization with your user present (another emailed PIN, or another device approval). The token response tells you which you hold: `"token_type":"DPoP"` is bound, `"token_type":"Bearer"` is not.

**Better than remembering: make forgetting impossible.** Register with `"dpop_bound_access_tokens": true` (RFC 9449 §5.2) and the token endpoint REFUSES to mint you an unbound token — a mint carrying no proof answers `invalid_dpop_proof` instead of quietly handing back a `Bearer` you cannot use here and cannot upgrade. On the device path especially, where the poll that binds you is whichever one your user happens to approve, a per-poll refusal you can see beats a silent unbound grant you discover later.

**Binding cuts both ways: a bound grant's refresh needs a proof too.** `grant_type=refresh_token` is another `POST https://craft.everygoodwork.io/-/oauth/token`, so it takes the same minting claims shown below, freshly signed, from the same key. Send it bare an hour in and it answers `invalid_grant` — which is also the code a revoked grant returns, so read `error_description` rather than the code: `DPoP proof required to refresh a sender-constrained token` means sign one and retry, and nothing has been revoked.

**The proof** is a compact ES256 JWS you sign per request with a P-256 key you generate and keep — at this door the token is inert to anyone without that private key, which is the whole point. At this door only: `/mcp` takes the very same token as a plain Bearer and never asks for a proof, so guard the token itself as closely as the key.

```json
header, both proofs
  {"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":"…","y":"…"}}

claims, minting at https://craft.everygoodwork.io/-/oauth/token  (the exchange, or the device poll)
  {"htm":"POST","htu":"https://craft.everygoodwork.io/-/oauth/token","iat":1730000000,"jti":"<unique per proof>"}

claims, calling https://craft.everygoodwork.io/-/user/agent
  {"htm":"POST","htu":"https://craft.everygoodwork.io/-/user/agent","iat":1730000000,"jti":"<unique per proof>",
   "ath":"<base64url(SHA-256(access_token))>","nonce":"<see below>"}
```

The two claim sets differ only as shown: `htu` is the exact URL you are calling, and `ath` + `nonce` are required at `/-/user/agent` and ignored when minting. All three segments are base64url without padding, joined `header.claims.signature`. The signature must be raw r‖s, 64 bytes (IEEE P1363) — `openssl` emits DER and will never verify; WebCrypto's `crypto.subtle.sign({name:"ECDSA",hash:"SHA-256"}, …)`, in Node or a browser, emits the right form. `iat` must be within 60 seconds of server time either way, and `jti` must be unique per proof — at `/-/user/agent` a repeated one is refused as a replay.

**Expect one 401 on your first call — it is a handshake, not a rejection.** A proof must carry a nonce we issued (RFC 9449 §8), so the first attempt answers 401 with a `DPoP-Nonce` response header and a `WWW-Authenticate: DPoP` challenge. That header is the only one of its kind on this door — a 401 carrying it is always this handshake. Re-sign the same proof with its value as `nonce` and a fresh `jti`, send it again, and it goes through. Reuse the nonce for later calls — they live 300 seconds — and re-sign whenever another 401 with that header hands you a new one. Minting carries no nonce dance at all: this handshake belongs to `/-/user/agent` alone.

Send the credential here as `Authorization: DPoP <access_token>`, never `Bearer`, with the proof in the `DPoP` header and `Content-Type: application/json`.

The body is `{"code": "<an ES module, as a string>", "capabilities": ["Craft:call"]}`.

**`Craft:call` is the entire catalog in one capability.** Declare it and `this.env.Craft` reaches every tool in the catalog under its exact `/mcp` name — `create_construct`, `author_construct_facet`, `publish_construct`, all of them. There is no capability per tool: `Construct:create_construct` does not exist and asking for it is refused with *Capability not permitted*. Ask for `Craft:call`. `search` and `execute` are `/mcp`'s two door NAMES, not catalog entries, so calling either from inside a program answers `unknown tool` — a program IS the execute rail, and `this.env.catalog` is what `search` reads.

`call(name, argsJson)` takes the tool name plus its arguments **as a JSON string**, and answers with a JSON string `{"text":"…","isError":false}`. Parse it and branch on `isError` — a refusal arrives that way, never as a thrown error.

```js
import { WorkerEntrypoint } from "cloudflare:workers";
export default class extends WorkerEntrypoint {
  async run() {
    const call = async (name, args) =>
      JSON.parse(await this.env.Craft.call(name, JSON.stringify(args)));
    const made = await call("create_construct",
      { name: "Connect Four", players: 2, turns: "Sequential", cardinality: "Spawner" });
    if (made.isError) return made.text;
    return made.text; // names your new construct — author it next with author_construct_facet
  }
}
```

Your program may default-export a `WorkerEntrypoint` whose async `run()` does the work, or a bare `async (craft, catalog) => …`; whatever it returns comes back to you as `{"ok":true,"result":…}`. `catalog` is the whole tool catalog as data — `[{ name, title, description, inputSchema }]`, the same array `/mcp`'s `search` reads — so a program discovers a tool's exact arguments without any list being sent to you first. It has no network access at all — the capabilities you declared are its only reach.

Creating anything needs the `publish` scope, so ask for `read write publish` at authorization. Without it `create_construct` answers `isError` and says which scope is missing.

**Creating is not gated on credit — but what you create spends it.** Nothing on the create path reads a balance; a construct bills afterwards, for the DO storage, media bytes and execution it actually uses. The credit arrives with the account: there is no automatic grant, so the voucher code that bought the account at sign-in is also the balance it starts with. There is one kind of credit, so what that code carried is only how much: a redeemed code spends and funds exactly like a deposit, a sponsorship, or revenue you earn through the platform. The privileges that ask for a funded account (locking your source with `set_construct_visibility` → `protected`, or listing a `sale` price with `set_construct_price`) answer no only until the account has actually taken in a dollar — from any of them, in any mix. Spend it down and a 5-day grace window follows, after which the constructs you own are taken offline: commands refused, viewers disconnected, while your facet code, event history and uploaded media all survive. You cannot top it up on your own initiative — `redeem_voucher` spends another code a human hands you, and deposits and sponsorships are made in a browser — so when it runs out, tell your user and ask, rather than retrying.

**Buying what CRAFT hosts needs no account at all.** An account is what it takes to CREATE and OWN constructs, and every one of them is paid for — by an invitation code, or by your user buying one at /-/auth/wallet. Consuming one is a wallet transaction, not an account one: a priced invoke, play pass, copy or ware answers 402 with its exact terms, you sign an EIP-3009 authorization for that amount, and you are served — no registration, no voucher, no human in the loop. If what you came to do is call somebody's construct and pay for the call, stay on that rail; you are a customer, not a signup.

## Worth knowing

- Every error response from this API carries an `agent_instructions` object telling you what broke and the exact next call to make. Read it instead of guessing.
- You cannot read your user's email. Step 6 always requires them.
- An account is only for CREATING and OWNING constructs, and every one is paid for — the code that opens one belongs to your user, whether they were given it or bought it, and is not something you can obtain or generate. Using what CRAFT already hosts needs no account and no code: a priced construct answers 402 with its terms, you sign the payment, you are served.
- A browser is never required for OTP. The device flow (step 3) IS browser-based by design — that's the point of offering it, not a fallback to avoid.
- Access tokens last 1 hour; refresh with grant_type=refresh_token. If you bound the grant with DPoP, that refresh needs its own freshly signed proof from the same key (the same shape you minted with); without one it answers invalid_grant — the same code a revoked grant returns, so read error_description, which names the remedy. Nothing is revoked: sign a proof and retry.
- The terms are republished from time to time, and every account re-consents when they are — so a verb that worked yesterday can be refused for terms today. There is no browser step: run a program calling `craft.read_terms({})` for the live text and the hash identifying it, then `craft.accept_terms({ hash })` to record your acceptance of exactly that version; the refusal itself names both.
- Both doors run PROGRAMS over the same tool catalog, and differ only in the credential: /mcp takes a plain Bearer token, and its two tools are `search` (a program over the catalog as data) and `execute` (a program that calls the catalog as you). The /-/user/agent code-mode gateway requires a DPoP sender-constrained token (RFC 9449) instead — bind it by sending a DPoP proof on the request that MINTS the token: step 7's exchange on the email path, or step 3's poll on the device path (every poll, freshly signed; the poll that returns the token is the one that binds it). There is no later upgrade: refreshing preserves an unbound token, so reaching that door afterwards costs a fresh authorization with your user present. Its one capability is `Craft:call`; see `code_mode`.

## Machine-readable

The same content as JSON: `GET https://craft.everygoodwork.io/.well-known/agent-onboarding`

The same setup as an imperative prompt to follow top to bottom: `GET https://craft.everygoodwork.io/-/prompt.md`
