Back to skills
SkillHub ClubShip Full StackFull StackBackend

gen-paylink-govilo

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/.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
3,087
Hot score
99
Updated
March 20, 2026
Overall rating
C4.0
Composite score
4.0
Best-practice grade
B78.7

Install command

npx @skill-hub/cli install openclaw-skills-gen-paylink-govilo

Repository

openclaw/skills

Skill path: skills/hau823823/gen-paylink-govilo

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/.

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack, Backend.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: openclaw.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install gen-paylink-govilo into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/openclaw/skills before adding gen-paylink-govilo to shared team environments
  • Use gen-paylink-govilo for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
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: [email protected]
  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.


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### references/setup-guide.md

```markdown
# 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.

```

### references/bot-api-quick-ref.md

```markdown
# 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 |

```



---

## Skill Companion Files

> Additional files collected from the skill directory layout.

### _meta.json

```json
{
  "owner": "hau823823",
  "slug": "gen-paylink-govilo",
  "displayName": "Gen Paylink Govilo",
  "latest": {
    "version": "1.0.1",
    "publishedAt": 1770948465348,
    "commit": "https://github.com/openclaw/skills/commit/e116920f7a6c76870ec41362632be7829b4f45bd"
  },
  "history": [
    {
      "version": "1.0.0",
      "publishedAt": 1770902688228,
      "commit": "https://github.com/openclaw/skills/commit/7df24ce0a71c8661c8c71697f0bbc3080e6185ff"
    },
    {
      "version": "0.1.2",
      "publishedAt": 1770901818475,
      "commit": "https://github.com/openclaw/skills/commit/a378a5d406a7e621e0ac7b023fe00635ab4a120b"
    },
    {
      "version": "0.1.0",
      "publishedAt": 1770880389660,
      "commit": "https://github.com/openclaw/skills/commit/b4788c6bc4b55128ef71ae04bc536eb72ddbe8dd"
    }
  ]
}

```

### scripts/api_client.py

```python
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)

```

### scripts/config.py

```python
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:
    api_key = cli_api_key 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(api_key=api_key, seller_address=seller_address)

```

### scripts/packager.py

```python
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

```

### scripts/workflow_create.py

```python
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(api_key=cfg.api_key, 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)

```