Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions cmd/obol/sell.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"os"
"os/signal"
"runtime"
"strconv"
"strings"
"syscall"
"time"

"github.com/ObolNetwork/obol-stack/internal/config"
"github.com/ObolNetwork/obol-stack/internal/enclave"
Expand Down Expand Up @@ -39,6 +42,7 @@ func sellCommand(cfg *config.Config) *cli.Command {
sellStatusCommand(cfg),
sellStopCommand(cfg),
sellDeleteCommand(cfg),
sellProbeCommand(cfg),
sellPricingCommand(cfg),
sellRegisterCommand(cfg),
},
Expand Down Expand Up @@ -724,6 +728,119 @@ func sellDeleteCommand(cfg *config.Config) *cli.Command {
// sell pricing
// ---------------------------------------------------------------------------

// ---------------------------------------------------------------------------
// sell probe — verify a sold endpoint is live and returns 402
// ---------------------------------------------------------------------------

func sellProbeCommand(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "probe",
Usage: "Verify a sold endpoint is live and payment-gated",
ArgsUsage: "<name>",
Description: `Probes the public tunnel URL for a ServiceOffer and checks that it
returns HTTP 402 with valid x402 pricing headers. This confirms the
endpoint is reachable, payment-gated, and correctly configured.

Examples:
obol sell probe my-api -n default
obol sell probe my-llm -n llm`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "namespace",
Aliases: []string{"n"},
Usage: "Namespace of the ServiceOffer",
Required: true,
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
if cmd.NArg() == 0 {
return fmt.Errorf("name required: obol sell probe <name> -n <ns>")
}
name := cmd.Args().First()
ns := cmd.String("namespace")

// 1. Get tunnel URL.
tunnelURL, err := tunnel.GetTunnelURL(cfg)
if err != nil {
return fmt.Errorf("tunnel not available: %w\nStart with: obol tunnel restart", err)
}

// 2. Determine the service path from x402 pricing config.
urlPath := fmt.Sprintf("/services/%s", name)
pricingCfg, err := x402verifier.GetPricingConfig(cfg)
if err == nil {
for _, r := range pricingCfg.Routes {
if r.OfferNamespace == ns && r.OfferName == name {
urlPath = strings.TrimSuffix(r.Pattern, "/*")
urlPath = strings.TrimSuffix(urlPath, "*")
break
}
}
}

probeURL := strings.TrimRight(tunnelURL, "/") + urlPath + "/health"
fmt.Printf("Probing %s ...\n", probeURL)

// 3. Send unauthenticated request — expect 402.
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Get(probeURL)
if err != nil {
return fmt.Errorf("probe failed: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)

switch resp.StatusCode {
case http.StatusPaymentRequired:
// Parse x402 pricing from body.
var pricing struct {
X402Version int `json:"x402Version"`
Accepts []struct {
Scheme string `json:"scheme"`
PayTo string `json:"payTo"`
Amount string `json:"maxAmountRequired"`
Asset string `json:"asset"`
} `json:"accepts"`
}
if err := json.Unmarshal(body, &pricing); err != nil {
fmt.Printf(" Status: 402 Payment Required (could not parse pricing body)\n")
return nil
}

fmt.Printf(" Status: 402 Payment Required ✓\n")
fmt.Printf(" x402: v%d\n", pricing.X402Version)
if len(pricing.Accepts) > 0 {
a := pricing.Accepts[0]
fmt.Printf(" Price: %s USDC (%s)\n", a.Amount, a.Scheme)
fmt.Printf(" PayTo: %s\n", a.PayTo)
}
fmt.Printf(" URL: %s\n", strings.TrimSuffix(probeURL, "/health"))
fmt.Println("\nEndpoint is live and payment-gated.")
return nil

case http.StatusOK:
fmt.Printf(" Status: 200 OK (NOT payment-gated!)\n")
fmt.Printf(" The endpoint is reachable but x402 middleware is not active.\n")
fmt.Printf(" Check: obol sell status %s -n %s\n", name, ns)
return fmt.Errorf("endpoint is not payment-gated")

case http.StatusNotFound:
fmt.Printf(" Status: 404 Not Found\n")
fmt.Printf(" The route may not be published yet.\n")
fmt.Printf(" Check: obol sell status %s -n %s\n", name, ns)
return fmt.Errorf("endpoint not found")

default:
fmt.Printf(" Status: %d %s\n", resp.StatusCode, http.StatusText(resp.StatusCode))
if len(body) > 0 && len(body) < 500 {
fmt.Printf(" Body: %s\n", string(body))
}
return fmt.Errorf("unexpected status %d", resp.StatusCode)
}
},
}
}

func sellPricingCommand(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "pricing",
Expand Down
233 changes: 233 additions & 0 deletions internal/embed/skills/monetize-guide/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
---
name: monetize-guide
description: "End-to-end guide for monetizing GPU resources or HTTP services through obol-stack. Covers pre-flight checks, model detection, pricing research, selling via x402, ERC-8004 registration, and verification. Use this skill when the user wants to monetize their machine."
metadata: { "openclaw": { "emoji": "🚀", "requires": { "bins": ["python3"] } } }
---

# Monetize Guide

Step-by-step guide to expose local GPU resources or HTTP services as x402 payment-gated endpoints with on-chain discovery via ERC-8004.

## When to Use

- User says "monetize my machine", "sell my GPU", or "expose my service for payments"
- Setting up a new paid endpoint from scratch
- Need to figure out what to sell and at what price

## When NOT to Use

- Managing existing offers (list/status/delete) — use `sell` directly
- Buying inference from others — use `buy-inference`
- Cluster diagnostics — use `obol-stack`

## Workflow

Follow these phases in order. Stop and ask the user for confirmation before executing phase 4.

### Phase 1: Pre-flight Checks

Verify the environment is ready before proceeding.

```bash
# 1. Check cluster is running
obol kubectl get nodes

# 2. Check the agent is initialized (has RBAC for monetization)
obol kubectl get clusterrolebinding openclaw-monetize-read-binding -o jsonpath='{.subjects}'

# 3. Get the wallet address (auto-generated by agent)
python3 /data/.openclaw/skills/ethereum-local-wallet/scripts/signer.py accounts
```

**If cluster is not running**: Tell the user to run `obol stack up` first. Do NOT run it yourself — it takes several minutes and changes system state.

**If agent has no RBAC subjects**: Tell the user to run `obol agent init`.

**If no wallet address**: The wallet is created during `obol stack up`. If missing, the stack may not have completed setup.

### Phase 2: Detect What Can Be Sold

#### For GPU / Inference

```bash
# Check what Ollama models are available locally
curl -s http://localhost:11434/api/tags | python3 -c "
import json, sys
data = json.load(sys.stdin)
for m in data.get('models', []):
size_gb = m.get('size', 0) / 1e9
print(f\" {m['name']:30s} {size_gb:.1f} GB\")
"
```

Report the available models to the user. If no models are found, suggest they pull one:
```bash
ollama pull qwen3.5:4b # Small, fast
ollama pull qwen3.5:9b # Medium, good quality
```

#### For HTTP Services

```bash
# List services in the cluster that could be monetized
obol kubectl get svc -A --no-headers | grep -v 'kube-system\|traefik\|x402\|monitoring\|erpc\|obol-frontend'
```

Ask the user which service they want to expose and on which port.

### Phase 3: Research Pricing

Query the ERC-8004 registry to see what comparable services charge.

```bash
# Search for registered agents on Base Sepolia
python3 /data/.openclaw/skills/discovery/scripts/discovery.py search --limit 10

# For each agent with x402Support, fetch their registration to see pricing
python3 /data/.openclaw/skills/discovery/scripts/discovery.py uri <agent_id>
```

**Pricing guidelines** (present these to the user with your research):

| Service Type | Typical Range | Notes |
|-------------|---------------|-------|
| LLM inference (small, <4B) | 0.0005–0.002 USDC/req | Fast, low compute |
| LLM inference (medium, 4-14B) | 0.001–0.005 USDC/req | Good quality/cost balance |
| LLM inference (large, >14B) | 0.005–0.02 USDC/req | High quality, slower |
| Data API / indexer | 0.0001–0.001 USDC/req | Depends on query complexity |
| Compute-heavy (GPU hours) | 0.10–1.00 USDC/hour | Fine-tuning, training |

**Always present your research and recommendation to the user and ask them to confirm the price before proceeding.**

### Phase 4: Sell the Service

Only proceed after the user has confirmed the price.

#### Inference (Ollama model)

```bash
obol sell inference <name> \
--model <model_name> \
--price <confirmed_price> \
--register \
--register-name "<descriptive name>" \
--register-description "<what the model does>" \
--register-skills natural_language_processing/text_generation/chat_completion \
--register-domains technology/artificial_intelligence
```

The `--wallet` and `--chain` will be auto-resolved (remote-signer wallet, base-sepolia default).

#### HTTP Service

```bash
obol sell http <name> \
--upstream <service_name> \
--namespace <namespace> \
--port <port> \
--per-request <confirmed_price> \
--chain base-sepolia \
--health-path <health_endpoint> \
--register \
--register-name "<descriptive name>" \
--register-description "<what the service does>" \
--register-skills <oasf_skill_path> \
--register-domains <oasf_domain_path>
```

### Phase 5: Wait for Reconciliation

The agent reconciler automatically processes the ServiceOffer through 6 stages.

```bash
# Watch the conditions progress (check every 15 seconds, up to 2 minutes)
for i in $(seq 1 8); do
echo "--- Attempt $i ---"
obol sell status <name> -n <namespace> 2>&1 | grep -A1 'type:\|status:\|reason:\|message:'
sleep 15
done
```

Expected progression:
1. ModelReady → True (instant for HTTP, may take minutes if pulling a model)
2. UpstreamHealthy → True
3. PaymentGateReady → True
4. RoutePublished → True
5. Registered → True (best-effort, non-blocking)
6. Ready → True

If a stage is stuck, check:
```bash
# Agent logs for reconciliation errors
obol kubectl logs -l app=openclaw -n openclaw-obol-agent --tail=50

# x402-verifier is running
obol kubectl get pods -n x402
```

### Phase 6: Verify the Endpoint

```bash
# Get the tunnel URL
obol tunnel status

# Probe the endpoint (should return 402 with pricing)
obol sell probe <name> -n <namespace>

# Or manually:
TUNNEL_URL=$(obol tunnel status 2>&1 | grep -o 'https://[^ ]*')
curl -s -o /dev/null -w "%{http_code}" "$TUNNEL_URL/services/<name>/health"
# Expected: 402

# Inspect the 402 pricing response
curl -s "$TUNNEL_URL/services/<name>/health" | python3 -m json.tool
```

A 402 response with `x402Version: 1` and an `accepts` array confirms the endpoint is live and payment-gated.

### Phase 7: Report to User

Present a summary:

```
Service monetized successfully!

Name: <name>
Model: <model> (if inference)
Price: <price> USDC per request
Chain: base-sepolia
Wallet: <wallet_address>
Endpoint: <tunnel_url>/services/<name>/v1/chat/completions
Registry: Agent #<id> on ERC-8004 (Base Sepolia)

Buyers can discover this service at:
<tunnel_url>/.well-known/agent-registration.json

To check status: obol sell status <name> -n <namespace>
To stop selling: obol sell stop <name> -n <namespace>
To delete: obol sell delete <name> -n <namespace>
```

## OASF Skills & Domains Reference

Use these when registering for on-chain discovery:

**Common skills**:
- `natural_language_processing/text_generation/chat_completion` — LLM chat
- `natural_language_processing/text_generation` — general text generation
- `data_management/indexing` — data indexing services
- `data_management/search` — search services
- `machine_learning/model_optimization` — training/fine-tuning

**Common domains**:
- `technology/artificial_intelligence` — AI services
- `technology/artificial_intelligence/research` — ML research
- `technology/blockchain` — blockchain data services

## Constraints

- **Always confirm pricing with the user** — never set a price autonomously
- **Do NOT run `obol stack up`** without explicit user request — it's a long-running infra change
- **Do NOT run `obol stack down` or `obol stack purge`** — destructive operations
- **Registration is best-effort** — services become Ready even if on-chain mint fails (wallet may lack gas funds)
- **Tunnel URL changes on restart** — registration docs auto-update on next reconcile
Loading
Loading