diff --git a/cmd/obol/sell.go b/cmd/obol/sell.go index 36978a8..d880f32 100644 --- a/cmd/obol/sell.go +++ b/cmd/obol/sell.go @@ -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" @@ -39,6 +42,7 @@ func sellCommand(cfg *config.Config) *cli.Command { sellStatusCommand(cfg), sellStopCommand(cfg), sellDeleteCommand(cfg), + sellProbeCommand(cfg), sellPricingCommand(cfg), sellRegisterCommand(cfg), }, @@ -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: "", + 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 -n ") + } + 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", diff --git a/internal/embed/skills/monetize-guide/SKILL.md b/internal/embed/skills/monetize-guide/SKILL.md new file mode 100644 index 0000000..ab3619c --- /dev/null +++ b/internal/embed/skills/monetize-guide/SKILL.md @@ -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 +``` + +**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 \ + --model \ + --price \ + --register \ + --register-name "" \ + --register-description "" \ + --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 \ + --upstream \ + --namespace \ + --port \ + --per-request \ + --chain base-sepolia \ + --health-path \ + --register \ + --register-name "" \ + --register-description "" \ + --register-skills \ + --register-domains +``` + +### 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 -n 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 -n + +# Or manually: +TUNNEL_URL=$(obol tunnel status 2>&1 | grep -o 'https://[^ ]*') +curl -s -o /dev/null -w "%{http_code}" "$TUNNEL_URL/services//health" +# Expected: 402 + +# Inspect the 402 pricing response +curl -s "$TUNNEL_URL/services//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: + Model: (if inference) + Price: USDC per request + Chain: base-sepolia + Wallet: + Endpoint: /services//v1/chat/completions + Registry: Agent # on ERC-8004 (Base Sepolia) + +Buyers can discover this service at: + /.well-known/agent-registration.json + +To check status: obol sell status -n +To stop selling: obol sell stop -n +To delete: obol sell delete -n +``` + +## 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 diff --git a/internal/embed/skills/monetize-guide/references/seller-prompt.md b/internal/embed/skills/monetize-guide/references/seller-prompt.md new file mode 100644 index 0000000..d4bcba9 --- /dev/null +++ b/internal/embed/skills/monetize-guide/references/seller-prompt.md @@ -0,0 +1,102 @@ +# Seller Onboarding Prompt + +Use this prompt with Claude Code to set up monetization for any service on obol-stack. Copy and adapt it for your specific use case. + +--- + +## Prompt Template + +``` +You are helping me monetize a service on my obol-stack cluster. + +## My Setup + +- Cluster: running (obol stack up completed) +- Service type: [inference / http] +- Service details: [describe what you're selling] + +## What I Need + +1. Check my cluster is ready (nodes, agent RBAC, wallet) +2. Detect available [models / services] and recommend what to sell +3. Research comparable pricing on the ERC-8004 registry +4. Propose a price and ask me to confirm +5. Create the ServiceOffer and wait for it to reach Ready +6. Verify the endpoint returns a proper 402 to unauthenticated requests +7. Show me the final public URL and how buyers interact with it + +## Constraints + +- Use the `monetize-guide` skill for the end-to-end flow +- Always ask me to confirm pricing before proceeding +- Register the service on ERC-8004 for on-chain discovery +- Use OASF skills/domains that match my service type +``` + +--- + +## Service Type Guidance + +### Inference (Ollama models) + +For monetizing a local LLM: + +- **Upstream**: Ollama at `localhost:11434` (auto-detected) +- **Pricing model**: `--per-request` or `--per-mtok` +- **Registration skills**: `natural_language_processing/text_generation/chat_completion` +- **Registration domains**: `technology/artificial_intelligence` +- **Buyer interaction**: OpenAI-compatible API at `/services//v1/chat/completions` +- **Health check**: Ollama `/api/tags` (auto-configured) + +### HTTP API (generic service) + +For monetizing any HTTP service running in the cluster: + +- **Upstream**: Kubernetes service name + port +- **Pricing model**: `--per-request` +- **Registration skills**: Choose from OASF taxonomy based on what the service does +- **Registration domains**: Choose from OASF taxonomy based on the domain +- **Buyer interaction**: REST API at `/services//*` +- **Health check**: Must have a health endpoint (default: `/health`) + +### Compute (GPU hours) + +For selling GPU compute time (fine-tuning, training): + +- **Upstream**: Worker API (e.g., autoresearch worker at port 8080) +- **Pricing model**: `--per-hour` +- **Registration skills**: `machine_learning/model_optimization` +- **Registration domains**: `technology/artificial_intelligence/research` +- **Buyer interaction**: Experiment submission API at `/services//experiment` +- **Health check**: `/health` or `/healthz` +- **Note**: `--per-hour` is approximated to per-request at 5 min/request for x402 gating + +--- + +## Post-Sell Monitoring + +After the service is live, periodically check: + +```bash +# Service status and conditions +obol sell status -n + +# Verify endpoint is payment-gated +obol sell probe -n + +# Check x402 verifier logs for payment activity +obol kubectl logs -l app=x402-verifier -n x402 --tail=20 + +# Check tunnel is active +obol tunnel status +``` + +### Troubleshooting + +| Symptom | Cause | Fix | +|---------|-------|-----| +| ServiceOffer stuck at UpstreamHealthy | Upstream not reachable | Check pod is running, service exists, port is correct | +| ServiceOffer stuck at PaymentGateReady | x402 namespace not ready | Check `obol kubectl get pods -n x402` | +| 404 instead of 402 at endpoint | HTTPRoute not created | Check RoutePublished condition, verify Traefik gateway | +| 200 instead of 402 (no payment gate) | Pricing route missing | Check x402-pricing ConfigMap in x402 namespace | +| Registration failed | Wallet not funded | Non-blocking; service still works without on-chain registration |