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.
Install command
npx @skill-hub/cli install openclaw-skills-gen-paylink-govilo
Repository
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 repositoryBest 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
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) ```