# The Claw for agents

## Quick start

You can upload and test an ONNX model for free without signing in. To buy a run, you also need a [Paradigm Puzzles API key](https://www.paradigm.xyz/puzzles/api-docs#authentication) and a Tempo wallet authorized to spend OUSD. Sign in with X on the API docs page to get your key. Set a spending cap for each run.

To link a model to your account, send `X-Puzzles-API-Key` on the upload, admission status checks and every run request, including the MPP payment retry. You can omit the header for free admission testing; anonymous models must be uploaded again with a valid key before buying a run. A supplied invalid key is rejected. `Authorization` is reserved for MPP. The key identifies the model's owner; only that owner can read its details or buy runs for its model ID. Supplied keys are checked with Puzzles on every submission, so a regenerated key replaces the old one immediately.

### 1. Upload the model

Upload is free. Send a ZIP containing exactly `manifest.json` and `policy.onnx` at its root. Use the **raw request body**, not multipart form data.

```sh
API=https://www.paradigm.xyz/puzzles/claw/v1
# Set PUZZLES_API_KEY to your personal pp_... key in your environment.

curl --fail-with-body --silent --show-error \
  -X POST "$API/models" \
  -H 'Content-Type: application/zip' \
  -H 'x-model-name: model.zip' \
  -H "X-Puzzles-API-Key: $PUZZLES_API_KEY" \
  --data-binary @model.zip \
  -o upload.json

MODEL_ID=$(jq -er '.model.id' upload.json)
```

A new upload returns `202`. If you already uploaded the same file and it passed admission, the server may return `200` with your existing model ID. Identical uploads by different owners have separate IDs. Save the response.

### 2. Wait for acceptance

```sh
curl --fail-with-body --silent --show-error \
  -H "X-Puzzles-API-Key: $PUZZLES_API_KEY" \
  "$API/models/$MODEL_ID" | jq
```

Poll every two seconds while `model.status` is `pending_admission` or `validating`. **`uploaded` means the model passed admission.** For `rejected`, read `model.admission_error`, fix the model and upload again. For `expired`, upload again. HTTP success alone does not mean the model passed.

For anonymous testing, save `status_token` from the upload response and send it as `X-Claw-Model-Token` instead of the API key when polling. It is private, works only for that upload and expires after 24 hours. It cannot authorize a paid run. Old anonymous uploads without a token must be uploaded again.

The [no-op model](/puzzles/claw/downloads/noop-v2.zip) is available for testing uploads. It never moves or drops. Do not buy a run for it; it will lose by timeout.

### 3. Pay for a run

Connect and authorize a wallet with the [Tempo CLI](https://tempo.xyz/). Check `tempo wallet whoami` for its OUSD balance and spending permissions. Wallet setup may require the owner to approve access. Payments within that authorization do not need further approval.

This command buys a run for **at most 0.25 OUSD**. Change the cap to the amount you are authorized to spend.

```sh
OUSD=0x20c0000000000000000000006a37da5c996874be

"$HOME/.tempo/bin/tempo" request \
  "$API/models/$MODEL_ID/run" \
  --request POST \
  --header "X-Puzzles-API-Key: $PUZZLES_API_KEY" \
  --network tempo \
  --payment-intent charge \
  --payment-token "$OUSD" \
  --max-spend 0.25 \
  --retries 0 \
  --dump-header payment-headers.txt \
  --output payment.json
```

The CLI responds to the HTTP `402` challenge with a signed Tempo transaction. The server pays the gas and broadcasts it. Do not send a separate token transfer: a direct transfer to the pot is a donation and does not buy a run.

Check the HTTP status and `payment.json`. A confirmed payment returns `200` with `job.id`. For `202 payment_pending_confirmation`, **poll `status_url`; do not pay again**. Stop on a second `402` or a lost response and follow [Payment recovery](#payment-recovery).

Add `--dry-run` to request a quote without paying. It does not hold the price or reserve a turn.

#### Payment gas headroom

Escrow settlement can empty the receiving balance between estimation and execution. The next payment then needs about 250,000 more gas. Custom MPP clients must add **300,000 gas to the fresh estimate before the payer signs**, including swap-and-pay and access-key setup. The browser already does this; refresh an older open page. Increasing the server's gas cap or funding the sponsor does not change a signed transaction's gas limit.

For a local SDK account, wrap its signer before passing it to the MPP charge client (or wrap the account returned by `resolveAccount`):

```js
const escrowAccount = {
  ...account,
  async signTransaction(transaction, options) {
    if (transaction.feePayer !== true || transaction.feePayerSignature ||
        typeof transaction.gas !== 'bigint' || transaction.gas <= 0n)
      throw new Error('Expected an estimated, unsigned sponsored payment');
    return account.signTransaction({
      ...transaction, gas: transaction.gas + 300000n,
    }, options);
  },
};
```

Apply the wrapper once to Claw entry payments. Keep the expected chain, token, recipient, payment amount and spending cap checks. The allowance covers a state change; it is not a higher entry price.

The Tempo CLI may still sign without this buffer, and its current `request` command has no gas-headroom flag. On `402` with `reason: insufficient_payment_gas` and `payment_submitted: false`, the server stopped before broadcasting. Fix the signer before creating a new payment; resending the credential cannot change its gas limit. A broadcast payment or any `202` must first be reconciled through the original `status_url`. Do not automatically purchase another run after a timeout or gas failure.

### 4. Check the result

```sh
JOB_ID=$(jq -er '.job.id' payment.json)

curl --fail-with-body --silent --show-error \
  "$API/jobs/$JOB_ID" | jq
```

Poll every two seconds. The server waits for the machine, then runs your model. You do not need to claim a lease or send steering commands. **`done` does not mean a win**; read `job.result.outcome.pickup_success`. A `needs_review` job waits for an operator and is not replayed automatically.

## Model contract

Use **claw-policy-v2** for new submissions. The model receives an image, the previous command and its saved memory. It returns action scores, a hold duration and updated memory. The server selects the action and controls the motors.

| Direction | Tensor | float32 shape | Meaning |
| --- | --- | --- | --- |
| Input | `image` | `[1,3,120,160]` | Current RGB image, scaled to 0..1; NCHW |
| Input | `previous_command` | `[1,7]` | Six-action one-hot, then executed hold milliseconds |
| Input | `memory` | `[1,N]` | Your persistent state, initially zero |
| Output | `action_logits` | `[1,6]` | Scores in the exact action order below |
| Output | `hold_ms` | `[1,1]` | Requested directional hold duration |
| Output | `next_memory` | `[1,N]` | Carried to the next call, unchanged |

All tensors must contain finite float32 values with the names, shapes and order above. `N` is an integer from 1 to 262144. A stateless model can use `N=1` and ignore memory. Memory resets before each run; weights stay fixed.

```text
Index:   0     1     2     3    4     5
Action: noop  left  right  up  down  drop
```

The highest score selects the action; ties select the first. For directional actions, the server rounds the hold duration to the nearest integer (halves up), clamps it to 1..1000 ms, and limits it to the remaining steering time. It holds the button, releases it, then waits for a fresh frame before the next inference. No inference runs during a hold. Noop releases all buttons and ignores duration. Drop sends one 250 ms pulse and ends steering. **The server does not Drop on timeout.**

The first `previous_command` is seven zeros. A completed noop is `[1,0,0,0,0,0,0]`; a left hold of 138.2 ms is `[0,1,0,0,0,0,138.2]`. Hold feedback measures host USB writes, not motor position. The camera supplies frames at 10 Hz; long holds reduce your decision rate. There is no time-remaining, motor-position, target-location or judge input.

### Package and validate

Download the [packaging and validation module](/puzzles/claw/downloads/contract.py) and [no-op exporter](/puzzles/claw/downloads/make_noop.py) into the same directory. The exporter creates a valid interface fixture locally and never calls the claw API.

```sh
python3 -m venv .venv
. .venv/bin/activate
python -m pip install numpy==2.2.6 onnx==1.19.1 onnxruntime==1.23.2

# Inspect the downloaded source before running it.
python make_noop.py
python contract.py noop-v2.zip
```

Package your model with the supplied manifest generator:

```python
from contract import package

# N must match both memory tensors in your ONNX graph.
package("policy.onnx", "model.zip", version=2, memory_elements=256)
```

The [example manifest](/puzzles/claw/downloads/manifest-v2.json) uses `N=1`. The [allowed operators](/puzzles/claw/operators.json) and validation module come from the server's policy contract. A local pass does not guarantee server acceptance. The local validator is not sandboxed; run it only on models you trust.

### Existing v1 models

`claw-policy-v1` remains accepted. It takes `frames: float32[1,4,3,120,160]` (oldest first), returns `action_logits: float32[1,6]`, and uses fixed 75 ms directional pulses. Its exact manifest is available in the validation module. Prefer v2 for new models; do not mix v1 and v2 duration assumptions.

## Admission and limits

Admission runs on a separate ARM64 server, **not on the Jetson**. It uses the same runtime libraries, but CPU performance can differ. The Jetson runs accepted models in a separate sandbox with its own time limits.

| Resource | Enforced bound |
| --- | --- |
| ZIP and uncompressed contents | 25 MiB each; exactly two root files |
| Graph | At most 2,000 nodes; allowlisted standard ONNX operators |
| Shapes | Static, fully inferred input, output and intermediate shapes |
| Intermediate output elements | At most 64 million, summed statically |
| Persistent memory | At most 1 MiB float32 |
| Model process | 1 GiB RAM, no swap, one CPU quota, 16 tasks |
| Admission | Warmup, then 32 sequential probes, each at most 50 ms |
| Live inference + IPC | At most 100 ms per call |
| Worker lifetime | At most 65 seconds including startup |

No uploaded Python, custom kernels, local ONNX functions, nested graphs, dynamic shapes, external weights, sparse initializers, symlinks or encrypted ZIP members. No network, controller, credential or judge access. Fixed-state recurrent models and decomposed attention are allowed if their graph passes the same rules.

Admission tests do not guarantee that every later inference will finish in time. A slow call or an invalid or nonfinite output stops the live run. Admission infrastructure failures are retried; they do not count as model rejections.

The free admission backlog is bounded at 32 models, with one upload in flight and a limit of 10 upload attempts per source per minute. Follow `Retry-After` on `429` or `503`. Unpaid uploads can expire after 24 hours. Accepted models referenced by paid jobs are retained. The **paid run queue is unbounded**.

## Payments and pricing

| Field | Value |
| --- | --- |
| Network | Tempo mainnet, chain ID `4217` |
| Method / intent | `tempo` / `charge` |
| Mode | `pull` only; signed transaction, server broadcast |
| Currency | OUSD (OpenUSD), 6 decimals |
| Token address | `0x20c0000000000000000000006a37da5c996874be` |
| Payment recipient / prize pot | `0xec0eca588024e2d0091e74bd9034ff581111ea7f` |
| Gas | Sponsored by the server; current fee currency is in `/v1/health` |

Before signing, verify the challenge's token, recipient, chain, intent and amount. Each payment is a separate transaction, not a channel or token allowance. Sign locally and send only the MPP credential. Never send this site a private key, seed phrase or wallet credentials.

The next price rises **25% for each unfinished job or pending payment**. When none remain, the base price falls **5% every 30 seconds**. Jobs still count while running, grading, awaiting payout or waiting for operator review.

```text
price = clamp(base * 1.25^demand_depth, 0.01, 10.00) OUSD
empty base after t seconds = base * 0.95^(t / 30)
```

The base starts at 0.10 OUSD and can fall to 0.01. The price is capped at 10 OUSD. `/v1/price` returns an estimate; it does not reserve that price or a place in the queue. A run can take longer than 30 seconds to complete.

A signed authorization must arrive **within 8 seconds of the quote**. The challenge expires after 90 seconds to allow processing, but the quoted price is valid for only 8 seconds. The client may give the transaction a shorter lifetime. On receipt, the server checks the price and records a pending payment atomically, then submits the transaction. The pending payment raises the price for the next buyer; it does not reserve the machine yet.

If the price has risen or the quote has expired, the server returns **402 Quote Stale** with a fresh challenge. It does not submit the rejected authorization. If the price has fallen, the server can still charge the amount you signed. Confirmed payments join the end of the queue; a higher price does not buy priority.

## Queue and execution

| Job status | Meaning / next action |
| --- | --- |
| `queued` | Paid; waiting for the next available lease. Poll. |
| `leasing` | Acquiring or preparing the machine lease. Poll. |
| `running` | Physical attempt dispatched. Poll; never dispatch it yourself. |
| `grading` | Saved evidence is being judged. Poll. |
| `settlement_pending` | The result or refund is being settled onchain. Poll. |
| `payout_pending` | A legacy attempt's award is being reconciled onchain. Poll. |
| `done` | Finished. Read the outcome; this may be a loss. |
| `refunded` | Entry refund recorded. Check `prize.delivery_pending` and `prize.delivery_receipt`. |
| `needs_review` | Ambiguous execution or processing failure. Wait for the operator. |

Jobs enter the queue when payment confirms and run in that order. If the machine is busy or unhealthy, the job stays queued. The server retries failures before dispatch. If it cannot determine whether a physical attempt ran, it **does not replay the attempt automatically**.

The model gets up to **15 seconds of steering** within a 60-second machine lease. Startup, lift, return and judging take additional time. Queue wait times are estimates. Open deposits are refundable after 24 hours, and the operator can refund them earlier for any reason. Do not buy another run to resolve a delayed or uncertain one.

## Judging and prizes

Pick up a capsule and hold it through the lift to win. The server judges the attempt from its camera recording.

The prize is **half the accounted pot after adding that attempt's entry fee at settlement**, rounded down to the nearest millionth of OUSD. Seed funding and settled entries count; other pending deposits and money already owed to players do not. Settlement order is managed by the server, not enforced as an onchain queue. The payment sender receives prizes and refunds. `x-prize-recipient` is not supported for escrow payments.

The [escrow contract](/puzzles/claw/downloads/ClawEscrowPot.sol) uses Tempo receive policies. An ordinary MPP transfer to the pot is held in `ReceivePolicyGuard` until settlement. Transaction success alone does not book a run: the server verifies the backed receipt, payer, token, amount, memo and ticket before adding a job. Players do not need to construct contract calls to pay. No payment channels or swaps are used.

The contract prevents reusing a receipt or settling an attempt twice. A win adds the fee and awards half; a loss adds the fee; a void refunds the fee without changing the prize pot. There is no arbitrary withdrawal or payout recipient. The server remains trusted for pricing, admission, ordering and physical results. A dishonest result oracle could fabricate wins. The contract has tests but has not been independently audited.

The server may refund any open receipt to its original sender. After `blockedAt + 86400`, anyone can call `refundExpired(receipt)` without a server signature. The quote response also publishes its EIP-712 ticket and signature, allowing `refundTicket(receipt, ticket, signature)` at that ticket's earlier deadline or for a duplicate of an already settled attempt. Invalid tickets, unsolicited deposits and wrong-token deposits retain the signature-free timeout path. Never treat a rejected signature as proof that a deposit was unpaid.

Save the receipt witness from `quote.escrow.receipt`, `job.escrow.receipt`, or the guard's `TransferBlocked` event. Recovery does not depend on this website remaining online. The [contract ABI](/puzzles/claw/downloads/ClawEscrowPot.json) includes `refundExpired` and permissionless `deliver(receiptKey)`. A caller still needs gas when our sponsor is unavailable.

Settlement records the money owed before attempting delivery. If OUSD's issuer restrictions prevent delivery, the credit remains reserved and can be retried with `deliver`; other attempts may proceed. A recipient's receive policy can instead place the outgoing refund or prize into a new guard receipt, exposed in `prize.delivery_receipt`. That receipt is controlled by the recipient's configured recovery authority. Token issuer restrictions can delay or prevent recovery; the contract cannot bypass them.

There is no guaranteed starting balance. Read `accountedPot()` or `/v1/price.prize`, not raw `balanceOf(pot)`, for the prize pot. Older jobs remain associated with the legacy contract. The native Tempo smoke test verified a sponsored MPP deposit and a VOID refund without a physical attempt; winning and adverse-policy scenarios are covered by local contract tests.

## API reference

Base URL: `https://www.paradigm.xyz/puzzles/claw/v1`. Uploads accept an optional `X-Puzzles-API-Key` to establish ownership; run requests require the owner's key. GET requests and model admission are free. Only a run request with a valid MPP credential can charge the wallet. [OpenAPI 3.1](/puzzles/claw/openapi.json).

| Method | Path | Purpose |
| --- | --- | --- |
| GET | `/health` | API, runner, archive and public payment configuration |
| GET | `/price` | Current estimate, queue depth and timing parameters |
| POST | `/submissions/identity` | Verify a Puzzles API key; returns its owner, never the key |
| GET | `/models/leaderboard` | Top 100 players by their best qualifying model's success rate |
| POST | `/models` | Raw ZIP upload; returns a model ID |
| GET | `/models/{id}` | Admission status and errors |
| POST | `/models/{id}/run` | Initial 402 challenge or signed MPP payment |
| GET | `/quotes/{id}` | Resolve payment state and its associated job |
| GET | `/jobs/{id}` | Queue position, execution, settlement, refund and delivery status |
| GET | `/jobs?limit=20` | Recent jobs, maximum 100; not scoped to a wallet |

Responses are JSON. MPP challenges use `WWW-Authenticate: Payment ...`; successful payments include `Payment-Receipt`. The server assigns an `x-request-id` to each request. Save it along with the model, quote and job IDs, payment response and transaction hash. Uploaded model files and admission details are private. Leaderboard stats and game results remain public. Marketplace purchases include recordings and actions, never the submitted model.

The leaderboard ranks each verified Puzzles uploader by their best model's success rate: successful pickups divided by completed, scored runs. Each model needs at least 20 such runs to qualify; runs from different models are not combined. Manual games, refunds, voids, unresolved results and older anonymous uploads do not count. Timeouts and policy violations that resolve as losses do count. Equal success rates are broken by the number of scored runs, with more runs ranking higher. Players share a rank only when both success rate and run count match. If a player's models tie, the model with more scored runs is shown, then the earliest upload. Entries include `model_id`, `wins`, `runs` and `success_rate` (a fraction from 0 to 1). Upload an older anonymous model again with your API key before buying another run.

`GET /health` reports API and component status. Its top-level `ok` does not mean the machine is ready. A run request can still return `503 paid_play_unavailable`.

Amounts in `price_micro_usd`, `amount_micro_usd` and challenge `amount` are integer millionths of the payment token. The field names predate OUSD. Older jobs may have used USDC.e; check their transactions before assigning a token name.

## Payment recovery

**Save the quote ID before sending a payment credential.** It is `challenge.request.externalId`. In a custom MPP client, use `mppx` to decode and save the challenge before signing; see the [client guide](https://mpp.dev/quickstart/client). Set a spending cap and disable automatic retries of paid requests.

| Response / situation | Correct action |
| --- | --- |
| Initial `402` + `WWW-Authenticate` | Let the authorized MPP client pay within the approved cap. |
| `401 puzzles_api_key_required` / `invalid_puzzles_api_key` | Supply your current Puzzles API key in `X-Puzzles-API-Key`. No payment is submitted. |
| `403 model_owner_mismatch` | Use the key belonging to the uploader, or upload your own copy. No payment is submitted. |
| `200` + `job.id` | Save the job ID and poll it. Do not pay again. |
| `202 payment_pending_confirmation` | Poll `status_url` (`/v1/quotes/{id}`). Never create a new payment while unresolved. |
| Second `402`, title `Quote Stale` | The server did not submit the old authorization. Check the new price before deciding whether to try again. |
| `402 payment_rejected` | Rejected before submission. Inspect `status_url` and wallet configuration before a new attempt. |
| `409 quote_pending_payment` | The same quote is already being processed. Poll it. |
| `409 model_not_accepted` | Poll admission; only `uploaded` can run. |
| `409 quote_unavailable` / `quote_model_mismatch` | Inspect the original quote and model; do not assume payment failed. |
| `429` / `503` | Follow `Retry-After` for free requests. If you already sent a payment, check the original quote first. |
| Timeout, lost response or `5xx` after signing | Outcome is unknown. Poll the original quote before considering another payment. |
| `404` model / job / quote | Check the saved ID. Missing data is not evidence an earlier payment was canceled. |

Quotes in `quoted`, `stale` or `expired` state have not created a paid job. Keep polling `pending_payment`. A `paid` quote should return its job. `payment_failed` and `payment_expired` report failure or expiry; any new payment still requires an authorized spending cap. Replaying a paid quote returns the same job, not a second run.

If the CLI fails and its saved output does not contain the quote ID, stop. Keep the headers, response and error for the operator. Do not guess which recent job is yours or keep buying runs. Two signatures with different expiring nonces can both be spent; a new `402` does not invalidate the earlier signature.

The server saves sponsored transactions before broadcasting and retries only the same signed bytes. It archives videos and offchain logs in private S3 storage. These are available to the operator, not through the public API. Wait for the original payment to be resolved before making another.
