Skip to content
Merged
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
60 changes: 60 additions & 0 deletions .github/scripts/check_links.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""Check that relative .md links in the template's docs resolve to real files.

Scans README.md, CONTRIBUTING.md, and every .md under workspace/ and .openclaw/.
Skips anchors, external URLs, and any path under skills/opensea/ (mounted at
deploy time, not in this repo).
"""

from __future__ import annotations

import re
import sys
from pathlib import Path

REPO = Path(__file__).resolve().parents[2]
LINK_RE = re.compile(r"\[[^\]]+\]\(([^)\s]+)\)")
ROOTS = ["README.md", "CONTRIBUTING.md", "workspace", ".openclaw"]


def md_files() -> list[Path]:
files: list[Path] = []
for root in ROOTS:
p = REPO / root
if p.is_file():
files.append(p)
elif p.is_dir():
files.extend(p.rglob("*.md"))
return files


def is_external(target: str) -> bool:
return target.startswith(("http://", "https://", "mailto:", "#"))


def main() -> int:
broken: list[str] = []
for md in md_files():
text = md.read_text(encoding="utf-8")
for match in LINK_RE.finditer(text):
target = match.group(1).split("#", 1)[0]
if not target or is_external(target):
continue
if target.startswith("skills/opensea/"):
continue
resolved = (md.parent / target).resolve()
if not resolved.exists():
rel = md.relative_to(REPO)
broken.append(f"{rel} → {target}")

if broken:
print("Broken in-repo links:")
for b in broken:
print(f" {b}")
return 1
print("markdown links OK")
return 0


if __name__ == "__main__":
sys.exit(main())
83 changes: 83 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
name: validate

on:
pull_request:
push:
branches: [main]

jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Validate manifest.json
run: |
set -euo pipefail
jq . manifest.json > /dev/null

# Required top-level keys + types
jq -e '.version | type == "number"' manifest.json > /dev/null
jq -e '.agent | type == "object"' manifest.json > /dev/null
jq -e '.template | type == "object"' manifest.json > /dev/null
jq -e '.skills | type == "array"' manifest.json > /dev/null
jq -e '.scripts | type == "object"' manifest.json > /dev/null
jq -e '.routes | type == "array"' manifest.json > /dev/null

# Agent shape
jq -e '.agent.name | type == "string" and length > 0' manifest.json > /dev/null
jq -e '.agent.description | type == "string" and length > 0' manifest.json > /dev/null
jq -e '.agent.vibe | type == "string"' manifest.json > /dev/null
jq -e '.agent.emoji | type == "string"' manifest.json > /dev/null

# Template shape
jq -e '.template.slug | type == "string" and test("^[a-z0-9-]+$")' manifest.json > /dev/null
jq -e '.template.version | type == "string" and test("^[0-9]+\\.[0-9]+\\.[0-9]+$")' manifest.json > /dev/null
jq -e '.template.category | type == "string"' manifest.json > /dev/null
jq -e '.template.tags | type == "array" and length > 0' manifest.json > /dev/null
jq -e '.template.authorName | type == "string"' manifest.json > /dev/null

# Skills shape — every entry needs clawhub_slug + name
jq -e 'all(.skills[]; .clawhub_slug | type == "string" and test("^[a-z0-9-]+/[a-z0-9-]+$"))' manifest.json > /dev/null
jq -e 'all(.skills[]; .name | type == "string" and length > 0)' manifest.json > /dev/null

# Scripts shape — build is required (start is optional)
jq -e '.scripts.build | type == "string" and length > 0' manifest.json > /dev/null

echo "manifest.json OK"

- name: Validate .openclaw/openclaw.json
run: |
set -euo pipefail
jq . .openclaw/openclaw.json > /dev/null
jq -e '.compaction | type == "string"' .openclaw/openclaw.json > /dev/null
jq -e '.maxConcurrent | type == "number"' .openclaw/openclaw.json > /dev/null
echo ".openclaw/openclaw.json OK"

- name: Check required workspace files exist
run: |
set -euo pipefail
required=(
".openclaw/SOUL.md"
"workspace/SOUL.md"
"workspace/AGENTS.md"
"workspace/IDENTITY.md"
"workspace/TOOLS.md"
"workspace/BOOTSTRAP.md"
"workspace/HEARTBEAT.md"
"workspace/USER.md"
"README.md"
"LICENSE"
)
missing=0
for f in "${required[@]}"; do
if [ ! -f "$f" ]; then
echo "missing: $f"
missing=1
fi
done
[ "$missing" -eq 0 ]
echo "workspace files OK"

- name: Lint markdown — no broken in-repo links
run: python3 .github/scripts/check_links.py
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ node_modules/
.env
.env.local
.claude/
memory/*
!memory/.gitkeep
workspace/memory/*
!workspace/memory/.gitkeep
44 changes: 44 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Contributing

This is the **template** for the NFT Collector Copilot agent on Pinata. The OpenSea skill it depends on lives in [`ProjectOpenSea/opensea-skill`](https://github.com/ProjectOpenSea/opensea-skill) and is published to ClawHub as `opensea/opensea-marketplace`. Skill changes do **not** belong here — open them against that repo. Template changes (workspace docs, manifest, README) belong here.

## Repo layout

See `README.md` → *Repository layout*. The short version: `manifest.json` defines the agent, `.openclaw/` configures the harness, `workspace/` holds everything the agent reads at runtime.

## Making changes

1. Branch from `main`: `git checkout -b your-change`.
2. Edit. Most edits are to `workspace/*.md` or `manifest.json`.
3. Run validation locally before pushing — same checks CI runs:
```bash
jq . manifest.json > /dev/null
jq . .openclaw/openclaw.json > /dev/null
```
4. Open a PR. CI runs the same shape checks plus presence checks for required workspace files.

## Versioning

`manifest.json` has two version fields:

- `version` (top-level integer) — manifest **schema** version. Don't touch unless the schema itself changes.
- `template.version` (semver string) — this template's version. Bump on every change that ships to users:
- patch: doc tweaks, prompt fixes
- minor: new heartbeat steps, new memory schemas, new TOOLS.md fields
- major: breaking changes to memory layout, manifest shape, or env requirements

Tag releases on `main` after merge: `git tag v1.0.1 && git push --tags`.

## Style

Match the existing voice: terse, numerate, defers to the skill rather than duplicating it. The agent reads these files every session — every line costs context. Cut anything that isn't load-bearing.

## Don't commit

- Secrets, `.env*`, real API keys, real wallet IDs.
- `workspace/memory/*` — runtime state, gitignored.
- `.claude/` — local Claude Code settings.

## Questions

Open an issue or ping the OpenSea team.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Ozone Networks, Inc. (OpenSea)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ An AI agent for NFT collectors. Watches [OpenSea](https://opensea.io), scores ev

## The killer feature: **Whale-Cross Alerts**

Point it at a list of wallets you respect (vitalik.eth, your favorite PFP whale, whoever) and your watchlist. When a tracked whale buys, mints, or lists into any collection — whether it's on your watchlist or not — you get a one-line alert within a heartbeat, with a one-click follow gated by your Privy spend cap. Nobody else ships this because nobody else has the OpenSea Stream API, a TEE-enforced wallet, and a scoring rubric in one box.
Point it at a list of wallets you respect (vitalik.eth, your favorite PFP whale, whoever) and your watchlist. When a tracked whale buys, mints, or lists into any collection — whether it's on your watchlist or not — you get a one-line alert within a heartbeat, with a one-click follow gated by your Privy spend cap. Few others ship this — it takes the OpenSea events feed, a TEE-enforced wallet, and a disciplined scoring rubric in the same box.

## What it does

Expand Down Expand Up @@ -66,7 +66,7 @@ Full walkthrough: `skills/opensea/references/wallet-setup.md`. Policy templates:
- **Env-only credentials.** No private keys in the repo or agent workspace.
- **Privy policy is the hard ceiling.** The spend cap, destination allowlist, and chain filter are enforced inside a TEE before signing.
- **Per-turn confirmation for material actions.** Any buy, offer acceptance, approval, or transfer above `confirmAboveEth` (in `workspace/TOOLS.md`) needs explicit "yes" in the current turn. Snipes can bypass this only when the listing is fully inside your configured envelope — see `workspace/SOUL.md` → *Hierarchy of Ceilings*.
- **Sell-side always confirms.** The native-value Privy cap doesn't constrain WETH offer acceptances, so those always require per-turn approval regardless of price.
- **Sell-side always confirms.** Privy's native-value cap is denominated in the chain's native token (ETH, etc.) — it does **not** apply to WETH transfers, which is how Seaport offer acceptances pay out. Sells, offer acceptances, and ERC721/1155 approvals therefore always require per-turn confirmation, regardless of price.
- **Pre-Buy Gate.** Wash trades, thin markets, uneconomic gas, and fee surprises all block a buy before it's proposed.
- **Policy rejections surface verbatim.** No workarounds.

Expand All @@ -75,17 +75,19 @@ Full walkthrough: `skills/opensea/references/wallet-setup.md`. Policy templates:
```
.
├── manifest.json # Pinata agent manifest — attaches opensea/opensea-marketplace from ClawHub
├── LICENSE # MIT
├── .openclaw/
│ ├── openclaw.json
│ ├── openclaw.json # OpenClaw harness config (compaction, concurrency)
│ └── SOUL.md # short canonical persona — points at workspace/SOUL.md
└── workspace/
├── SOUL.md # guardrails + Conviction Score + Pre-Buy Gate
├── AGENTS.md # workspace conventions + memory schemas
├── IDENTITY.md # blank — filled on first run
├── TOOLS.md # watchlist, whales, budgets — user-tunable
├── BOOTSTRAP.md # first-run walkthrough
├── BOOTSTRAP.md # first-run walkthrough — agent deletes after completion
├── HEARTBEAT.md # idle-cycle routine
└── USER.md # collector profile — filled on first run
├── USER.md # collector profile — filled on first run
└── memory/ # created at runtime — floors, actions, taste, scan state
```

At deploy time Pinata attaches the OpenSea skill under `skills/opensea/` (SKILL.md + `references/*.md` + `scripts/*.sh`) — not checked into this repo.
Expand Down
4 changes: 2 additions & 2 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
},
"template": {
"slug": "nft-collector-copilot",
"version": "1.0.0",
"category": "actions & transactions",
"tags": ["nft", "opensea", "seaport", "privy", "collector", "defi"],
"authorName": "OpenSea",
Expand All @@ -18,8 +19,7 @@
{ "clawhub_slug": "opensea/opensea-marketplace", "name": "OpenSea" }
],
"scripts": {
"build": "npm install -g @opensea/cli",
"start": ""
"build": "npm install -g @opensea/cli"
},
"routes": []
}
11 changes: 11 additions & 0 deletions workspace/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ Avoids re-running `nfts list-by-account` on every heartbeat. Invalidate a chain'

So `events by-account` only streams the tail since last heartbeat.

### `memory/scan_state.json` — once-per-day scan timestamps

Tracks the last run of HEARTBEAT.md steps 5 (drops) and 6 (trending), which run at most every ~20 hours.

```json
{
"last_drop_scan": "2026-04-17T02:00:00Z",
"last_trending_scan": "2026-04-17T02:00:00Z"
}
```

### `MEMORY.md` (in `workspace/`, not `memory/`)

Free-text long-form observations that don't fit a schema: API quirks, volatile slugs, seller wallets that pattern wash-trade, drops that disappointed, specific things the user said about their taste that you want to remember word-for-word.
Expand Down
10 changes: 5 additions & 5 deletions workspace/HEARTBEAT.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,25 +45,25 @@ For each listing within `snipeThresholdEth`:

For each address in `TOOLS.md` → `whaleWallets`:

- `opensea --format toon events by-account <address> --limit 20` (skill command; falls back to `opensea-get.sh /api/v2/events/accounts/<address>`).
- `opensea --format toon events by-account <address> --limit 20`.
- Filter the tail since last heartbeat (track `memory/whale_cursor.json` per address).
- Events of interest: `sale` (whale bought), `item_received` (new NFT), `listing` (whale listing for sale).
- If the whale touched a collection in the watchlist, queue a high-priority alert. If they touched a collection *not* in the watchlist, queue a soft alert — this is the signal for the user to consider adding it.

### 5. Drop radar — once per day, not every heartbeat

Check `memory/last_drop_scan` timestamp. If > 20 hours since last scan:
Check `memory/scan_state.json` → `last_drop_scan`. If > 20 hours since last scan:

- `opensea --format toon drops list --type upcoming --limit 10`
- `opensea --format toon drops list --type featured --limit 10`
- Cross-reference against `memory/taste.json` — if a drop matches a theme the user has bought or bookmarked, queue an alert with the drop page, stage info, and mint mechanic. Save `last_drop_scan`.
- Cross-reference against `memory/taste.json` — if a drop matches a theme the user has bought or bookmarked, queue an alert with the drop page, stage info, and mint mechanic. Update `memory/scan_state.json` → `last_drop_scan`.

### 6. Trending scan — once per day

Check `memory/last_trending_scan`. If > 20 hours since last scan:
Check `memory/scan_state.json` → `last_trending_scan`. If > 20 hours since last scan:

- `opensea --format toon collections trending --timeframe one_day --chains ethereum,base --limit 10`
- For each trending slug not already watched, cross-reference against `taste.json`. Propose adding up to three with a one-line rationale; the user decides.
- For each trending slug not already watched, cross-reference against `taste.json`. Propose adding up to three with a one-line rationale; the user decides. Update `memory/scan_state.json` → `last_trending_scan`.

### 7. Alert dispatch

Expand Down
Empty file added workspace/memory/.gitkeep
Empty file.
Loading