Skip to content

Commit 97773f0

Browse files
committed
fix: x402 V2 helpers and accountless locus deploy flow in skill
1 parent 70261a1 commit 97773f0

File tree

2 files changed

+188
-85
lines changed

2 files changed

+188
-85
lines changed

.claude/skills/locus-wallet-deploy/SKILL.md

Lines changed: 161 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ argument-hint: "[bot-filename]"
99

1010
You are helping a user deploy their Turbine trading bot to Locus for 24/7 cloud operation.
1111

12-
This skill authenticates with Locus using the user's existing Turbine wallet via x402 (Polygon USDC payment). No separate Locus API key is needed — the user's TURBINE_PRIVATE_KEY handles everything.
12+
This skill authenticates with Locus using the user's existing Turbine wallet via x402 (Polygon USDC payment). No separate Locus account or API key is needed — the wallet payment creates a workspace and returns a JWT that works for all API calls.
1313

1414
Locus is a container platform that deploys services via a REST API. Each service costs $0.25/month from workspace credits. New workspaces start with $6.00 in credits (x402 sign-up costs 0.001 USDC). Services get an auto-subdomain at `svc-{id}.buildwithlocus.com` with HTTPS.
1515

16+
**Auth flow:** x402 wallet payment (0.001 USDC) → creates workspace + JWT → use JWT for all API calls (projects, services, source upload, deployments).
17+
1618
**API Documentation for Locus** `https://buildwithlocus.com/SKILL.md`
1719

1820
**Base URL:** `https://api.buildwithlocus.com/v1`
@@ -197,39 +199,51 @@ Run the x402 sign-up script:
197199
python scripts/locus_x402.py sign-up
198200
```
199201

200-
The script:
201-
1. POSTs to Locus's x402-sign-up endpoint
202-
2. Gets 402 response with payment requirements
203-
3. Signs an EIP-3009 USDC payment authorization with TURBINE_PRIVATE_KEY
204-
4. Retries with the signed payment
202+
The script performs the x402 handshake:
203+
1. POSTs to Locus's `/auth/x402-sign-up` endpoint with empty body `{}`
204+
2. Gets 402 response with `PAYMENT-REQUIRED` header (base64-encoded JSON with x402 V2 payment requirements in `accepts` array: scheme, network `eip155:137`, amount `1000`, USDC asset address, recipient `payTo` address)
205+
3. Signs an EIP-3009 `TransferWithAuthorization` typed-data message using TURBINE_PRIVATE_KEY (no on-chain tx needed)
206+
4. Retries the POST with the signed payment in the `PAYMENT-SIGNATURE` header (base64-encoded V2 PaymentPayload with `payload`, `accepted` echoing back the requirements)
205207
5. Returns a JWT (saved to /tmp/locus-token.txt) and workspace info
206208

207-
**Parse the JSON output** to extract: `jwt`, `workspaceId`, `isNewWorkspace`, `claimUrl`, `creditBalance`.
209+
**Parse the JSON output** to extract: `jwt`, `workspaceId`, `isNewWorkspace`, `claimUrl`.
208210

209211
Tell the user the result:
210212
- If `isNewWorkspace: true`: "Created new Locus workspace: {workspaceId}. Starting credit balance: $6.00."
211213
- If `isNewWorkspace: false`: "Reconnected to existing workspace: {workspaceId}."
212214

213-
If the response includes a `claimUrl`, tell the user:
214-
"You can optionally link an email to your Locus workspace for a permanent API key:
215-
{claimUrl}
216-
This isn't required — your wallet works for authentication."
215+
If the response includes a `claimUrl`, mention it as optional:
216+
> "You can optionally link an email to your workspace for free token refreshes and a permanent API key:
217+
> {claimUrl}
218+
> This is NOT required — your wallet handles everything."
217219
218220
If the script fails:
219221
- Check that TURBINE_PRIVATE_KEY is set correctly in .env
220222
- Ensure the wallet has at least 0.001 USDC on Polygon
221223
- Show the error output and STOP
222224

225+
**Get the workspace ID:**
226+
227+
```bash
228+
TOKEN=$(cat /tmp/locus-token.txt)
229+
230+
WORKSPACE_ID=$(curl -s -H "Authorization: Bearer $TOKEN" \
231+
https://api.buildwithlocus.com/v1/auth/whoami | jq -r '.workspaceId')
232+
233+
echo "Workspace ID: $WORKSPACE_ID"
234+
```
235+
236+
Save `WORKSPACE_ID` for later use.
237+
223238
**Check billing balance:**
224239

225240
```bash
226241
TOKEN=$(cat /tmp/locus-token.txt)
227242
curl -s -H "Authorization: Bearer $TOKEN" \
228-
$BASE_URL/billing/balance | jq '{creditBalance, totalServices, status}'
243+
https://api.buildwithlocus.com/v1/billing/balance | jq '{creditBalance, totalServices, status}'
229244
```
230245

231-
If `creditBalance` < 0.25, offer x402 top-up:
232-
"Insufficient credits ($X.XX). Each service costs $0.25/month."
246+
New workspaces start with $6.00 in credits. If `creditBalance` < 0.25, offer x402 top-up:
233247

234248
Use `AskUserQuestion` with options: "Top up $5 from wallet" / "Top up $1 from wallet" / "I'll add credits manually"
235249

@@ -239,20 +253,49 @@ If they choose to top up:
239253
python scripts/locus_x402.py top-up <amount>
240254
```
241255

256+
The top-up also returns a fresh JWT (30-day expiry), which the script saves to `/tmp/locus-token.txt`.
257+
242258
Tell the user: "Authenticated with Locus. Credit balance: $X.XX"
243259

260+
**JWT refresh:** The JWT expires after 30 days. To refresh, either:
261+
- Call `python scripts/locus_x402.py sign-up` again (costs 0.001 USDC, returns new JWT for same workspace)
262+
- Call `python scripts/locus_x402.py top-up <amount>` (also returns a fresh JWT)
263+
- If they claimed their workspace: use `POST /v1/auth/exchange` with the `claw_` API key (free)
264+
244265
---
245266

246267
## Step 4: Create Project, Environment, and Service
247268

248-
Run these API calls sequentially, communicating each step to the user:
269+
**IMPORTANT:** The claimed workspace may already have projects from a previous deployment. Always check for existing projects first before creating new ones.
270+
271+
### Check for existing projects:
272+
273+
```bash
274+
TOKEN=$(cat /tmp/locus-token.txt)
275+
276+
PROJECTS=$(curl -s -H "Authorization: Bearer $TOKEN" \
277+
https://api.buildwithlocus.com/v1/projects)
278+
279+
echo "$PROJECTS" | jq '.projects[] | {id, name}'
280+
```
281+
282+
If a project named "turbine-bot" already exists, **reuse it**. Extract its `PROJECT_ID` and skip project creation. Also check its environments:
283+
284+
```bash
285+
PROJECT_ID="<existing project id>"
286+
287+
curl -s -H "Authorization: Bearer $TOKEN" \
288+
"https://api.buildwithlocus.com/v1/projects/$PROJECT_ID/environments" | jq '.environments[] | {id, name}'
289+
```
290+
291+
If an environment exists, reuse it. Extract its `ENV_ID` and skip environment creation.
249292

250-
**Create a project:**
293+
### Create a project (only if none exists):
251294

252295
```bash
253296
TOKEN=$(cat /tmp/locus-token.txt)
254297

255-
PROJECT=$(curl -s -X POST $BASE_URL/projects \
298+
PROJECT=$(curl -s -X POST https://api.buildwithlocus.com/v1/projects \
256299
-H "Authorization: Bearer $TOKEN" \
257300
-H "Content-Type: application/json" \
258301
-d '{"name": "turbine-bot", "description": "Turbine prediction market trading bot"}')
@@ -261,12 +304,10 @@ PROJECT_ID=$(echo $PROJECT | jq -r '.id')
261304
echo "Project ID: $PROJECT_ID"
262305
```
263306

264-
If the project creation fails, check the error. If a project named "turbine-bot" already exists, list projects and ask the user whether to reuse it or create a new one with a different name.
265-
266-
**Create an environment:**
307+
### Create an environment (only if none exists):
267308

268309
```bash
269-
ENV=$(curl -s -X POST $BASE_URL/projects/$PROJECT_ID/environments \
310+
ENV=$(curl -s -X POST https://api.buildwithlocus.com/v1/projects/$PROJECT_ID/environments \
270311
-H "Authorization: Bearer $TOKEN" \
271312
-H "Content-Type: application/json" \
272313
-d '{"name": "production", "type": "production"}')
@@ -275,10 +316,12 @@ ENV_ID=$(echo $ENV | jq -r '.id')
275316
echo "Environment ID: $ENV_ID"
276317
```
277318

278-
**Create a service:**
319+
### Create a service (only if none exists):
320+
321+
If service creation returns `"Service \"trading-bot\" already exists in this environment"`, the service already exists. You'll need the service ID for env vars and monitoring. Since there's no direct list-services endpoint, the service ID can be obtained from the deployment output in Step 6 (the git push response includes deployment details with `serviceId`). Alternatively, try creating the service and if it already exists, proceed to Step 5.
279322

280323
```bash
281-
SERVICE=$(curl -s -X POST $BASE_URL/services \
324+
SERVICE=$(curl -s -X POST https://api.buildwithlocus.com/v1/services \
282325
-H "Authorization: Bearer $TOKEN" \
283326
-H "Content-Type: application/json" \
284327
-d '{
@@ -308,7 +351,7 @@ echo "Service URL: $SERVICE_URL"
308351

309352
Tell the user:
310353
```
311-
Project created: turbine-bot
354+
Project: turbine-bot
312355
Environment: production
313356
Service: trading-bot
314357
URL (once deployed): {SERVICE_URL}
@@ -379,60 +422,96 @@ If they choose to run locally, tell them to run `python {BOT_FILE}`, wait for it
379422

380423
---
381424

382-
## Step 6: Deploy via Git Push
425+
## Step 6: Upload Source and Deploy
383426

384-
Set up the Locus git remote and push the code.
427+
There are two ways to deploy code to Locus. **Try source upload first** (works with JWT, no extra account needed). If it fails, fall back to git push.
385428

386-
**Get the workspace ID:**
429+
### Primary: Source Upload (JWT auth)
430+
431+
Create a tar.gz archive of the deployment files, upload via API, then trigger a deployment:
387432

388433
```bash
434+
# Create source archive (exclude non-essential files)
435+
tar czf /tmp/turbine-bot-source.tar.gz \
436+
--exclude='.venv' --exclude='__pycache__' --exclude='.git' \
437+
--exclude='node_modules' --exclude='.env' --exclude='*.egg-info' \
438+
--exclude='.DS_Store' --exclude='data' \
439+
pyproject.toml turbine_client/ {BOT_FILE} locus_runner.py Dockerfile
440+
389441
TOKEN=$(cat /tmp/locus-token.txt)
390442

391-
WORKSPACE_ID=$(curl -s -H "Authorization: Bearer $TOKEN" \
392-
$BASE_URL/auth/whoami | jq -r '.workspaceId')
443+
# Upload source
444+
UPLOAD=$(curl -s -X POST https://api.buildwithlocus.com/v1/sources/upload \
445+
-H "Authorization: Bearer $TOKEN" \
446+
-F "file=@/tmp/turbine-bot-source.tar.gz")
393447

394-
echo "Workspace ID: $WORKSPACE_ID"
448+
S3_KEY=$(echo $UPLOAD | jq -r '.s3Key // .key // empty')
449+
echo "Source uploaded: $S3_KEY"
395450
```
396451

397-
**Add the Locus git remote:**
398-
399-
Check if a `locus` remote already exists first. If it does, update it. If not, add it.
452+
If source upload succeeds, trigger a deployment:
400453

401454
```bash
402-
# Remove existing locus remote if present
403-
git remote remove locus 2>/dev/null
455+
DEPLOY=$(curl -s -X POST https://api.buildwithlocus.com/v1/deployments \
456+
-H "Authorization: Bearer $TOKEN" \
457+
-H "Content-Type: application/json" \
458+
-d '{"serviceId": "'"$SERVICE_ID"'", "source": {"type": "s3", "s3Key": "'"$S3_KEY"'"}}')
404459

405-
TOKEN=$(cat /tmp/locus-token.txt)
406-
git remote add locus "https://x:${TOKEN}@git.buildwithlocus.com/${WORKSPACE_ID}/${PROJECT_ID}.git"
460+
DEPLOYMENT_ID=$(echo $DEPLOY | jq -r '.id')
461+
echo "Deployment triggered: $DEPLOYMENT_ID"
407462
```
408463

409-
**Important:** The deployment files (`locus_runner.py`, `Dockerfile`) and the bot file need to be committed before pushing — only tracked files are included in the git push archive. Create a commit with these files:
410-
464+
Clean up:
411465
```bash
412-
git add locus_runner.py Dockerfile {BOT_FILE}
413-
git commit -m "Add Locus deployment files"
466+
rm /tmp/turbine-bot-source.tar.gz
414467
```
415468

416-
**Push to deploy:**
469+
### Fallback: Git Push (requires `claw_` API key)
470+
471+
If source upload fails with 403, the user needs a `claw_` API key for git push deployment. This requires claiming their workspace.
472+
473+
Tell the user:
474+
> "Source upload isn't available for your account. To deploy via git push, you'll need to claim your Locus workspace.
475+
> Visit your claim URL: {claimUrl}
476+
> After claiming, you'll get a `claw_` API key. Paste it back here."
477+
478+
Once they have the `claw_` key:
479+
480+
```bash
481+
# Exchange claw_ key for JWT to get the workspace ID
482+
CLAW_KEY="<user's key>"
483+
CLAW_TOKEN=$(curl -s -X POST https://api.buildwithlocus.com/v1/auth/exchange \
484+
-H "Content-Type: application/json" \
485+
-d '{"apiKey": "'"$CLAW_KEY"'"}' | jq -r '.token')
486+
487+
# IMPORTANT: The claimed workspace may differ from the x402 workspace.
488+
# Use the claw_ key's workspace and re-check for existing projects.
489+
CLAW_WORKSPACE=$(curl -s -H "Authorization: Bearer $CLAW_TOKEN" \
490+
https://api.buildwithlocus.com/v1/auth/whoami | jq -r '.workspaceId')
417491

418-
Tell the user: "Pushing code to Locus. This will upload the source and trigger a build. Builds typically take 3-7 minutes."
492+
# If workspace differs, save the new JWT and re-run Steps 4-5 with it
493+
echo "$CLAW_TOKEN" > /tmp/locus-token.txt
494+
```
495+
496+
Set up git remote and push:
419497

420498
```bash
499+
git remote remove locus 2>/dev/null
500+
git remote add locus "https://x:${CLAW_KEY}@git.buildwithlocus.com/${CLAW_WORKSPACE}/${PROJECT_ID}.git"
501+
502+
# Deployment files must be committed
503+
git add locus_runner.py Dockerfile {BOT_FILE}
504+
git commit -m "Add Locus deployment files"
505+
421506
git push locus main
422507
```
423508

424-
If the push fails:
425-
- Authentication error → Your JWT may have expired. Refresh it:
426-
```bash
427-
python scripts/locus_x402.py sign-up
428-
TOKEN=$(cat /tmp/locus-token.txt)
429-
git remote set-url locus "https://x:${TOKEN}@git.buildwithlocus.com/${WORKSPACE_ID}/${PROJECT_ID}.git"
430-
git push locus main
431-
```
432-
- Branch error → Try `git push locus HEAD:main` if on a different branch
433-
- Remote error → Verify workspace and project IDs
509+
Extract the deployment ID from the push output:
510+
```
511+
-> trading-bot [deploy_xxxxx]
512+
```
434513

435-
After the push, note the deployment IDs from the push output.
514+
**IMPORTANT for redeployments:** When using git push, always push via `git push locus main` to redeploy. Do NOT use `POST /v1/deployments` alone — it does not include source code and will fail.
436515

437516
---
438517

@@ -465,25 +544,42 @@ Keep the user informed at each check:
465544
- `building` → "Building Docker image from source..."
466545
- `deploying` → "Container is starting, running health checks..."
467546
- `healthy` → "Deployment is live!"
468-
- `failed` → Check logs and report the error
547+
- `failed`**Don't panic — check the service runtime status first (see below)**
548+
549+
**IMPORTANT: "failed" deployment does NOT always mean the bot isn't running.** The ECS health check may time out during the initial credential registration and USDC approval phase (which can take 30-60 seconds), causing the deployment to be marked as "failed" even though the container is running successfully.
550+
551+
**If deployment shows `failed`, check the actual service status:**
552+
553+
```bash
554+
TOKEN=$(cat /tmp/locus-token.txt)
555+
556+
# Check if the service is actually running
557+
curl -s -H "Authorization: Bearer $TOKEN" \
558+
"https://api.buildwithlocus.com/v1/services/$SERVICE_ID?include=runtime" | jq '.runtime_instances'
559+
560+
# Check the public health endpoint directly
561+
curl -s -w "\nHTTP: %{http_code}\n" "https://svc-${SERVICE_ID#svc_}.buildwithlocus.com/health"
562+
```
563+
564+
If `runtime_instances.status` is `"running"` and/or the health endpoint returns 200, **the bot IS running** despite the "failed" deployment status. Tell the user it's live and skip to Step 8.
469565

470-
**If deployment fails**, fetch the last logs:
566+
**If the service is genuinely not running**, fetch the deployment logs:
471567

472568
```bash
473569
TOKEN=$(cat /tmp/locus-token.txt)
474570

475571
curl -s -H "Authorization: Bearer $TOKEN" \
476-
"$BASE_URL/deployments/$DEPLOYMENT_ID" | jq '.lastLogs'
572+
"https://api.buildwithlocus.com/v1/deployments/$DEPLOYMENT_ID/logs" | jq '.logs[-20:][].message'
477573
```
478574

479575
Common failure causes:
480-
- **Health check timeout:** The `/health` endpoint didn't respond in time. Check that `locus_runner.py` is starting the health server before the bot.
576+
- **Health check timeout during startup:** The bot's initial credential registration and USDC approval can take time. The health endpoint starts immediately but ECS may declare the task unhealthy before the bot stabilizes. This is usually recoverable — the container keeps running.
481577
- **Dependency install failed:** Missing packages in `pyproject.toml`.
482578
- **Dockerfile error:** Check the build logs for syntax issues.
483579

484580
Help the user fix the issue and redeploy with another `git push locus main`.
485581

486-
**IMPORTANT:** After deployment reaches `healthy`, the public URL may return 503 for up to 60 seconds while service discovery registers the container. This is normal. Tell the user to wait before testing the URL.
582+
**IMPORTANT:** After deployment reaches `healthy` (or the service shows as running), the public URL may return 503 for up to 60 seconds while service discovery registers the container. This is normal. Tell the user to wait before testing the URL.
487583

488584
---
489585

@@ -543,22 +639,19 @@ Track your bot's performance on the leaderboard:
543639

544640
If the user wants to redeploy after making changes to their bot:
545641

642+
**If using source upload (primary path):**
643+
1. Make changes to the bot file
644+
2. Re-run Step 6 source upload (create new tar.gz, upload, trigger deployment)
645+
3. The JWT may need refreshing if >30 days old: `python scripts/locus_x402.py sign-up`
646+
647+
**If using git push (fallback path):**
546648
1. Make changes to the bot file
547649
2. Commit the changes: `git add {BOT_FILE} && git commit -m "Update trading strategy"`
548650
3. Push to Locus: `git push locus main`
549651
4. Monitor the deployment as in Step 7
550652

551653
The service URL stays the same — Locus does a rolling update with zero downtime.
552654

553-
**If git push fails with authentication error:**
554-
Your JWT may have expired (30-day lifetime). Refresh it:
555-
```bash
556-
python scripts/locus_x402.py sign-up
557-
TOKEN=$(cat /tmp/locus-token.txt)
558-
git remote set-url locus "https://x:${TOKEN}@git.buildwithlocus.com/${WORKSPACE_ID}/${PROJECT_ID}.git"
559-
git push locus main
560-
```
561-
562655
---
563656

564657
## Cleanup

0 commit comments

Comments
 (0)