Skip to content

Commit ed3bedd

Browse files
authored
Merge pull request #2 from ProjectOpenSea/chore/template-polish
chore: pre-submission polish — accuracy, hygiene, CI
2 parents 5cc2f63 + 5aa6a92 commit ed3bedd

10 files changed

Lines changed: 235 additions & 14 deletions

File tree

.github/scripts/check_links.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#!/usr/bin/env python3
2+
"""Check that relative .md links in the template's docs resolve to real files.
3+
4+
Scans README.md, CONTRIBUTING.md, and every .md under workspace/ and .openclaw/.
5+
Skips anchors, external URLs, and any path under skills/opensea/ (mounted at
6+
deploy time, not in this repo).
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import re
12+
import sys
13+
from pathlib import Path
14+
15+
REPO = Path(__file__).resolve().parents[2]
16+
LINK_RE = re.compile(r"\[[^\]]+\]\(([^)\s]+)\)")
17+
ROOTS = ["README.md", "CONTRIBUTING.md", "workspace", ".openclaw"]
18+
19+
20+
def md_files() -> list[Path]:
21+
files: list[Path] = []
22+
for root in ROOTS:
23+
p = REPO / root
24+
if p.is_file():
25+
files.append(p)
26+
elif p.is_dir():
27+
files.extend(p.rglob("*.md"))
28+
return files
29+
30+
31+
def is_external(target: str) -> bool:
32+
return target.startswith(("http://", "https://", "mailto:", "#"))
33+
34+
35+
def main() -> int:
36+
broken: list[str] = []
37+
for md in md_files():
38+
text = md.read_text(encoding="utf-8")
39+
for match in LINK_RE.finditer(text):
40+
target = match.group(1).split("#", 1)[0]
41+
if not target or is_external(target):
42+
continue
43+
if target.startswith("skills/opensea/"):
44+
continue
45+
resolved = (md.parent / target).resolve()
46+
if not resolved.exists():
47+
rel = md.relative_to(REPO)
48+
broken.append(f"{rel}{target}")
49+
50+
if broken:
51+
print("Broken in-repo links:")
52+
for b in broken:
53+
print(f" {b}")
54+
return 1
55+
print("markdown links OK")
56+
return 0
57+
58+
59+
if __name__ == "__main__":
60+
sys.exit(main())

.github/workflows/validate.yml

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
name: validate
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches: [main]
7+
8+
jobs:
9+
validate:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
14+
- name: Validate manifest.json
15+
run: |
16+
set -euo pipefail
17+
jq . manifest.json > /dev/null
18+
19+
# Required top-level keys + types
20+
jq -e '.version | type == "number"' manifest.json > /dev/null
21+
jq -e '.agent | type == "object"' manifest.json > /dev/null
22+
jq -e '.template | type == "object"' manifest.json > /dev/null
23+
jq -e '.skills | type == "array"' manifest.json > /dev/null
24+
jq -e '.scripts | type == "object"' manifest.json > /dev/null
25+
jq -e '.routes | type == "array"' manifest.json > /dev/null
26+
27+
# Agent shape
28+
jq -e '.agent.name | type == "string" and length > 0' manifest.json > /dev/null
29+
jq -e '.agent.description | type == "string" and length > 0' manifest.json > /dev/null
30+
jq -e '.agent.vibe | type == "string"' manifest.json > /dev/null
31+
jq -e '.agent.emoji | type == "string"' manifest.json > /dev/null
32+
33+
# Template shape
34+
jq -e '.template.slug | type == "string" and test("^[a-z0-9-]+$")' manifest.json > /dev/null
35+
jq -e '.template.version | type == "string" and test("^[0-9]+\\.[0-9]+\\.[0-9]+$")' manifest.json > /dev/null
36+
jq -e '.template.category | type == "string"' manifest.json > /dev/null
37+
jq -e '.template.tags | type == "array" and length > 0' manifest.json > /dev/null
38+
jq -e '.template.authorName | type == "string"' manifest.json > /dev/null
39+
40+
# Skills shape — every entry needs clawhub_slug + name
41+
jq -e 'all(.skills[]; .clawhub_slug | type == "string" and test("^[a-z0-9-]+/[a-z0-9-]+$"))' manifest.json > /dev/null
42+
jq -e 'all(.skills[]; .name | type == "string" and length > 0)' manifest.json > /dev/null
43+
44+
# Scripts shape — build is required (start is optional)
45+
jq -e '.scripts.build | type == "string" and length > 0' manifest.json > /dev/null
46+
47+
echo "manifest.json OK"
48+
49+
- name: Validate .openclaw/openclaw.json
50+
run: |
51+
set -euo pipefail
52+
jq . .openclaw/openclaw.json > /dev/null
53+
jq -e '.compaction | type == "string"' .openclaw/openclaw.json > /dev/null
54+
jq -e '.maxConcurrent | type == "number"' .openclaw/openclaw.json > /dev/null
55+
echo ".openclaw/openclaw.json OK"
56+
57+
- name: Check required workspace files exist
58+
run: |
59+
set -euo pipefail
60+
required=(
61+
".openclaw/SOUL.md"
62+
"workspace/SOUL.md"
63+
"workspace/AGENTS.md"
64+
"workspace/IDENTITY.md"
65+
"workspace/TOOLS.md"
66+
"workspace/BOOTSTRAP.md"
67+
"workspace/HEARTBEAT.md"
68+
"workspace/USER.md"
69+
"README.md"
70+
"LICENSE"
71+
)
72+
missing=0
73+
for f in "${required[@]}"; do
74+
if [ ! -f "$f" ]; then
75+
echo "missing: $f"
76+
missing=1
77+
fi
78+
done
79+
[ "$missing" -eq 0 ]
80+
echo "workspace files OK"
81+
82+
- name: Lint markdown — no broken in-repo links
83+
run: python3 .github/scripts/check_links.py

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ node_modules/
22
.env
33
.env.local
44
.claude/
5-
memory/*
6-
!memory/.gitkeep
5+
workspace/memory/*
6+
!workspace/memory/.gitkeep

CONTRIBUTING.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Contributing
2+
3+
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.
4+
5+
## Repo layout
6+
7+
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.
8+
9+
## Making changes
10+
11+
1. Branch from `main`: `git checkout -b your-change`.
12+
2. Edit. Most edits are to `workspace/*.md` or `manifest.json`.
13+
3. Run validation locally before pushing — same checks CI runs:
14+
```bash
15+
jq . manifest.json > /dev/null
16+
jq . .openclaw/openclaw.json > /dev/null
17+
```
18+
4. Open a PR. CI runs the same shape checks plus presence checks for required workspace files.
19+
20+
## Versioning
21+
22+
`manifest.json` has two version fields:
23+
24+
- `version` (top-level integer) — manifest **schema** version. Don't touch unless the schema itself changes.
25+
- `template.version` (semver string) — this template's version. Bump on every change that ships to users:
26+
- patch: doc tweaks, prompt fixes
27+
- minor: new heartbeat steps, new memory schemas, new TOOLS.md fields
28+
- major: breaking changes to memory layout, manifest shape, or env requirements
29+
30+
Tag releases on `main` after merge: `git tag v1.0.1 && git push --tags`.
31+
32+
## Style
33+
34+
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.
35+
36+
## Don't commit
37+
38+
- Secrets, `.env*`, real API keys, real wallet IDs.
39+
- `workspace/memory/*` — runtime state, gitignored.
40+
- `.claude/` — local Claude Code settings.
41+
42+
## Questions
43+
44+
Open an issue or ping the OpenSea team.

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Ozone Networks, Inc. (OpenSea)
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ An AI agent for NFT collectors. Watches [OpenSea](https://opensea.io), scores ev
44

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

7-
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.
7+
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.
88

99
## What it does
1010

@@ -66,7 +66,7 @@ Full walkthrough: `skills/opensea/references/wallet-setup.md`. Policy templates:
6666
- **Env-only credentials.** No private keys in the repo or agent workspace.
6767
- **Privy policy is the hard ceiling.** The spend cap, destination allowlist, and chain filter are enforced inside a TEE before signing.
6868
- **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*.
69-
- **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.
69+
- **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.
7070
- **Pre-Buy Gate.** Wash trades, thin markets, uneconomic gas, and fee surprises all block a buy before it's proposed.
7171
- **Policy rejections surface verbatim.** No workarounds.
7272

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

9193
At deploy time Pinata attaches the OpenSea skill under `skills/opensea/` (SKILL.md + `references/*.md` + `scripts/*.sh`) — not checked into this repo.

manifest.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
},
99
"template": {
1010
"slug": "nft-collector-copilot",
11+
"version": "1.0.0",
1112
"category": "actions & transactions",
1213
"tags": ["nft", "opensea", "seaport", "privy", "collector", "defi"],
1314
"authorName": "OpenSea",
@@ -18,8 +19,7 @@
1819
{ "clawhub_slug": "opensea/opensea-marketplace", "name": "OpenSea" }
1920
],
2021
"scripts": {
21-
"build": "npm install -g @opensea/cli",
22-
"start": ""
22+
"build": "npm install -g @opensea/cli"
2323
},
2424
"routes": []
2525
}

workspace/AGENTS.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,17 @@ Avoids re-running `nfts list-by-account` on every heartbeat. Invalidate a chain'
5555

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

58+
### `memory/scan_state.json` — once-per-day scan timestamps
59+
60+
Tracks the last run of HEARTBEAT.md steps 5 (drops) and 6 (trending), which run at most every ~20 hours.
61+
62+
```json
63+
{
64+
"last_drop_scan": "2026-04-17T02:00:00Z",
65+
"last_trending_scan": "2026-04-17T02:00:00Z"
66+
}
67+
```
68+
5869
### `MEMORY.md` (in `workspace/`, not `memory/`)
5970

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

workspace/HEARTBEAT.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,25 +45,25 @@ For each listing within `snipeThresholdEth`:
4545

4646
For each address in `TOOLS.md``whaleWallets`:
4747

48-
- `opensea --format toon events by-account <address> --limit 20` (skill command; falls back to `opensea-get.sh /api/v2/events/accounts/<address>`).
48+
- `opensea --format toon events by-account <address> --limit 20`.
4949
- Filter the tail since last heartbeat (track `memory/whale_cursor.json` per address).
5050
- Events of interest: `sale` (whale bought), `item_received` (new NFT), `listing` (whale listing for sale).
5151
- 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.
5252

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

55-
Check `memory/last_drop_scan` timestamp. If > 20 hours since last scan:
55+
Check `memory/scan_state.json``last_drop_scan`. If > 20 hours since last scan:
5656

5757
- `opensea --format toon drops list --type upcoming --limit 10`
5858
- `opensea --format toon drops list --type featured --limit 10`
59-
- 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`.
59+
- 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`.
6060

6161
### 6. Trending scan — once per day
6262

63-
Check `memory/last_trending_scan`. If > 20 hours since last scan:
63+
Check `memory/scan_state.json``last_trending_scan`. If > 20 hours since last scan:
6464

6565
- `opensea --format toon collections trending --timeframe one_day --chains ethereum,base --limit 10`
66-
- 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.
66+
- 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`.
6767

6868
### 7. Alert dispatch
6969

workspace/memory/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)