Skill: hau823823/gen-paylink-govilo (version 1.0.1)

--- name: gen-paylink-govilo description: > Upload files to Govilo and generate unlock links via Bot API. Use when: (1) Creating a Govilo unlock link from a ZIP, folder, or individual files, (2) Automating file upload to Govilo R2 storage with presigned URLs, (3) Managing Govilo Bot API interactions (presign → upload → create item). Requires GOVILO_API_KEY and SELLER_ADDRESS env vars. If missing, guides user to register at https://govilo.xyz/. metadata: author: [REDACTED_SECRET] version: "1.0" openclaw: requires: env: - GOVILO_API_KEY - SELLER_ADDRESS primaryEnv: GOVILO_API_KEY homepage: https://github.com/hau823823/gen-paylink-govilo --- # Govilo To Go Turn any file into a paid unlock link — one command to package, upload, and collect crypto payments. The last mile of automation: from creation to monetization. ## Before Running Always ask the user for these values before executing the CLI — never guess or use placeholders: 1. **title** — What is the product name? 2. **price** — How much to charge (in USDC)? 3. **description** — Short description of the product (optional, but always ask) ## CLI Command > Requires [uv](https://docs.astral.sh/uv/). See [references/setup-guide.md](references/setup-guide.md) for install instructions. Run from this skill's base directory. Use a **dedicated** env file containing only `GOVILO_API_KEY` (and optionally `SELLER_ADDRESS`). Never point `--env-file` at a project `.env` that contains unrelated secrets. ```bash cd <skill_base_directory> uv run --env-file <path_to>/.env.govilo create-link \ --input <path> \ --title "Product Name" \ --price "5.00" \ --address "0x..." \ --description "optional" ``` If no `.env.govilo` exists, create one before running: ```dotenv GOVILO_API_KEY=sk_live_xxx SELLER_ADDRESS=0x... ``` `--input` accepts ZIP file, folder, or individual files (repeatable). Non-ZIP inputs are auto-packaged. All output is JSON `{"ok": true/false, ...}` with exit code 1 on failure. ## Parameters | Param | Required | Source | Description | | --------------- | -------- | -------------------------- | -------------------------- | | `--input` | Yes | CLI (repeatable) | ZIP, folder, or file paths | | `--title` | Yes | CLI | Product title | | `--price` | Yes | CLI | Price in USDC | | `--address` | No | CLI > `SELLER_ADDRESS` env | Seller EVM wallet | | `--description` | No | CLI | Product description | ## Workflow 1. Validate config (API Key + seller address) 2. Package inputs → ZIP (if not already ZIP) 3. `POST /api/v1/bot/uploads/presign` → get upload_url + session_id 4. `PUT upload_url` → upload ZIP to R2 5. `POST /api/v1/bot/items` → get unlock_url ## File Limits - Max ZIP size: 20 MB - Max files in ZIP: 20 ## Setup Two values are required: | Variable | Required | Description | | ---------------- | -------- | ---------------------------------------- | | `GOVILO_API_KEY` | Yes | Bot API key from [govilo.xyz][] | | `SELLER_ADDRESS` | Yes* | EVM wallet address on **Base chain** | [govilo.xyz]: https://govilo.xyz/ *`SELLER_ADDRESS` can also be passed via `--address` CLI parameter. See [references/setup-guide.md](references/setup-guide.md) for step-by-step registration and wallet setup instructions. ## API Reference See [references/bot-api-quick-ref.md](references/bot-api-quick-ref.md) for Bot API endpoints and error codes.

path: pyproject.toml
content: [project]
name = "govilo-to-go"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = ["requests"]

[project.scripts]
create-link = "scripts.workflow_create:main"

[project.optional-dependencies]
dev = ["pytest"]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["scripts"]

[tool.pytest.ini_options]
testpaths = ["tests"]


path: references/bot-api-quick-ref.md
content: # Bot API Quick Reference

Base URL: `https://api.unlock.govilo.xyz`

## Endpoints

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/api/v1/bot/uploads/presign` | Bearer sk_live_xxx | Get presigned upload URL |
| POST | `/api/v1/bot/items` | Bearer sk_live_xxx | Confirm upload and create item |

## Presign Request

```json
POST /api/v1/bot/uploads/presign
{"seller_address": "0x..."}
```

Response: `upload_url`, `session_id`, `object_path`, `expires_at`

## Upload

```
PUT <upload_url>
Content-Type: application/zip
Body: <raw ZIP bytes>
```

## Create Item Request

```json
POST /api/v1/bot/items
{"session_id": "...", "title": "...", "price": "5.00", "description": "..."}
```

Response: `id`, `unlock_url`, `file_count`, `total_size`, `upload_status`

## Error Codes

| Code | HTTP | Message |
|------|------|---------|
| 809108001 | 401 | invalid api key |
| 809108002 | 401 | api key has been revoked |
| 809108003 | 401 | api key has expired |
| 809108005 | 429 | daily api key usage limit exceeded |
| 809108006 | 429 | api key rate limit exceeded |
| 809104001 | 404 | upload session not found |
| 809104002 | 410 | upload session expired |
| 809104003 | 404 | file not found |


path: references/setup-guide.md
content: # Setup Guide

## Prerequisites

This skill requires [uv](https://docs.astral.sh/uv/) — a fast Python package manager and runner.

### Install uv

    # macOS / Linux
    curl -LsSf https://astral.sh/uv/install.sh | sh

    # Windows
    powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

    # Or via Homebrew
    brew install uv

`uv run` automatically resolves Python >=3.11 and the `requests` dependency from `pyproject.toml` — no manual `pip install` needed.

## Required Values

Two values are required before using this tool:
- `GOVILO_API_KEY` — Bot API authentication token
- `SELLER_ADDRESS` — Your wallet address on Base chain

## 1. API Key

### Get Your API Key

1. Go to https://govilo.xyz/
2. Sign up for an account (Gmail auth login)
3. Open the left sidebar menu → click **Settings**
4. Scroll to the **API Key** section → click **Manage API Key**
5. Create a new key and copy it (format: `sk_live_xxx`)

### Configure

Create a **dedicated** env file (e.g. `.env.govilo`) containing only Govilo credentials:

    # .env.govilo
    GOVILO_API_KEY=sk_live_xxx

> **Important:** Do not use a shared project `.env` that may contain unrelated secrets.
> Always use a dedicated file like `.env.govilo` to avoid accidental leakage.

### Verify

    cd skills/gen-paylink-govilo
    uv run --env-file .env.govilo create-link --help

If the key is missing or invalid, the tool outputs:

    {"ok": false, "error": "API Key not configured. ..."}

## 2. Seller Address

### Requirements

- **Chain:** Base (Base Mainnet only, Chain ID: 8453)
- **Format:** EVM address — `0x` + 40 hex characters
- Example: `0x1234567890abcdef1234567890abcdef12345678`

### Get Your Address

**MetaMask:**

1. Install MetaMask browser extension (https://metamask.io/)
2. Create or import a wallet
3. Switch network to **Base** (add if not listed: Chain ID `8453`, RPC `https://mainnet.base.org`)
4. Click the account name at the top to copy your address

**Coinbase Wallet:**

1. Install Coinbase Wallet (https://www.coinbase.com/wallet)
2. Create or import a wallet
3. Switch network to **Base**
4. Tap **Receive** → copy your address

### Configure

**Option A — Environment variable** (recommended for repeated use):

Add to your `.env.govilo` file:

    SELLER_ADDRESS=0xYourWalletAddress

**Option B — CLI parameter** (per-command override):

    create-link --address "0xYourWalletAddress" ...

CLI `--address` takes priority over `SELLER_ADDRESS` env var.

> **Tip:** You can also set a **Default Payout Address** on the Govilo website
> (Settings → Default Payout Address → paste your `0x...` address → **Save**).
> This address is pre-filled when creating new links on the web UI, but the CLI
> still requires `SELLER_ADDRESS` env var or `--address` parameter.


path: scripts/api_client.py
content: from pathlib import Path

import requests


class ApiError(Exception):
    def __init__(self, message: str, code: int | None = None):
        super().__init__(message)
        self.code = code


class GoviloClient:
    def __init__(self, api_key: str, base_url: str):
        self._base_url = base_url.rstrip("/")
        self._headers = {
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
        }

    def _check_response(self, resp: requests.Response) -> dict:
        body = resp.json()
        if body.get("code", 0) != 0:
            raise ApiError(body.get("msg", "unknown error"), body.get("code"))
        return body["data"]

    def presign(self, seller_address: str) -> dict:
        resp = requests.post(
            f"{self._base_url}/api/v1/bot/uploads/presign",
            headers=self._headers,
            json={"seller_address": seller_address},
        )
        return self._check_response(resp)

    def upload(self, upload_url: str, zip_path: Path) -> None:
        with open(zip_path, "rb") as f:
            resp = requests.put(
                upload_url,
                headers={"Content-Type": "application/zip"},
                data=f,
            )
        if resp.status_code != 200:
            raise ApiError(f"Upload failed: HTTP {resp.status_code}")

    def create_item(
        self,
        session_id: str,
        title: str,
        price: str,
        description: str = "",
    ) -> dict:
        resp = requests.post(
            f"{self._base_url}/api/v1/bot/items",
            headers=self._headers,
            json={
                "session_id": session_id,
                "title": title,
                "price": price,
                "description": description,
            },
        )
        return self._check_response(resp)


path: scripts/config.py
content: from dataclasses import dataclass
import os


BASE_URL = "https://api.unlock.govilo.xyz"


class ConfigError(Exception):
    pass


@dataclass(frozen=True)
class Config:
    api_key: str
    seller_address: str
    base_url: str = BASE_URL


def load_config(
    cli_api_key: str | None = None,
    cli_address: str | None = None,
) -> Config:
    [REDACTED_SECRET] or os.environ.get("GOVILO_API_KEY")
    if not api_key:
        raise ConfigError(
            "API Key not configured. See references/setup-guide.md for registration steps. "
            "Register at https://govilo.xyz/ then set GOVILO_API_KEY=sk_live_xxx"
        )

    seller_address = cli_address or os.environ.get("SELLER_ADDRESS")
    if not seller_address:
        raise ConfigError(
            "Seller address required (Base chain). Use --address 0x... or set SELLER_ADDRESS env var. "
            "See references/setup-guide.md for wallet setup"
        )

    return Config([REDACTED_SECRET], seller_address=seller_address)


path: scripts/packager.py
content: import tempfile
import zipfile
from pathlib import Path

MAX_ZIP_SIZE = 20 * 1024 * 1024  # 20 MB
MAX_FILE_COUNT = 20


class PackageError(Exception):
    pass


def _validate_zip(zp: Path) -> None:
    if zp.stat().st_size > MAX_ZIP_SIZE:
        raise PackageError(f"ZIP file exceeds 20 MB limit")
    with zipfile.ZipFile(zp) as zf:
        if len(zf.namelist()) > MAX_FILE_COUNT:
            raise PackageError(f"ZIP contains more than {MAX_FILE_COUNT} files")


def _zip_paths(paths: list[Path], dest: Path) -> None:
    with zipfile.ZipFile(dest, "w", zipfile.ZIP_DEFLATED) as zf:
        for p in paths:
            zf.write(p, p.name)


def package(inputs: list[str]) -> Path:
    paths = [Path(p) for p in inputs]

    for p in paths:
        if not p.exists():
            raise PackageError(f"Path not found: {p}")

    # Single .zip file — passthrough
    if len(paths) == 1 and paths[0].suffix == ".zip":
        _validate_zip(paths[0])
        return paths[0]

    # Single directory — zip its contents
    if len(paths) == 1 and paths[0].is_dir():
        files = [f for f in paths[0].rglob("*") if f.is_file()]
        if len(files) > MAX_FILE_COUNT:
            raise PackageError(f"Directory contains more than {MAX_FILE_COUNT} files")
        dest = Path(tempfile.mktemp(suffix=".zip"))
        with zipfile.ZipFile(dest, "w", zipfile.ZIP_DEFLATED) as zf:
            for f in files:
                zf.write(f, f.relative_to(paths[0]))
        _validate_zip(dest)
        return dest

    # Multiple files — zip them together
    if len(paths) > MAX_FILE_COUNT:
        raise PackageError(f"Input exceeds {MAX_FILE_COUNT} files limit")
    dest = Path(tempfile.mktemp(suffix=".zip"))
    _zip_paths(paths, dest)
    _validate_zip(dest)
    return dest


path: scripts/workflow_create.py
content: import argparse
import json
import sys
from pathlib import Path

from scripts.api_client import GoviloClient, ApiError
from scripts.config import load_config, ConfigError
from scripts.packager import package, PackageError


def _output(data: dict) -> None:
    print(json.dumps(data, ensure_ascii=False))


def _fail(error: str, code: int | None = None) -> None:
    out = {"ok": False, "error": error}
    if code is not None:
        out["code"] = code
    _output(out)
    sys.exit(1)


def main() -> None:
    parser = argparse.ArgumentParser(description="Upload files and create Govilo unlock link")
    parser.add_argument("--input", required=True, action="append", dest="inputs", help="ZIP, folder, or file path (repeatable)")
    parser.add_argument("--title", required=True, help="Product title")
    parser.add_argument("--price", required=True, help="Price in USDC (e.g. 5.00)")
    parser.add_argument("--address", default=None, help="Seller EVM wallet address (overrides SELLER_ADDRESS env)")
    parser.add_argument("--description", default="", help="Product description")
    args = parser.parse_args()

    try:
        cfg = load_config(cli_api_key=None, cli_address=args.address)
    except ConfigError as e:
        _fail(str(e))

    try:
        zip_path = package(args.inputs)
    except PackageError as e:
        _fail(str(e))

    is_temp = zip_path not in [Path(p) for p in args.inputs]
    try:
        client = GoviloClient([REDACTED_SECRET], base_url=cfg.base_url)

        try:
            presign_data = client.presign(cfg.seller_address)
            client.upload(presign_data["upload_url"], zip_path)
            item_data = client.create_item(
                session_id=presign_data["session_id"],
                title=args.title,
                price=args.price,
                description=args.description,
            )
        except ApiError as e:
            _fail(str(e), code=e.code)

        _output({
            "ok": True,
            "unlock_url": item_data["unlock_url"],
            "item_id": item_data["id"],
            "file_count": item_data["file_count"],
            "total_size": item_data["total_size"],
        })
    finally:
        if is_temp:
            zip_path.unlink(missing_ok=True)


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)