Skip to content

Commit 9ee9d48

Browse files
committed
feat: add demo GIF + adoption instrumentation (badges, UTM, metrics)
1 parent 84d48a0 commit 9ee9d48

5 files changed

Lines changed: 175 additions & 10 deletions

File tree

README.md

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# floe-guard
22

3+
[![PyPI version](https://img.shields.io/pypi/v/floe-guard.svg)](https://pypi.org/project/floe-guard/)
4+
[![Downloads](https://static.pepy.tech/badge/floe-guard/month)](https://pepy.tech/project/floe-guard)
5+
[![Python versions](https://img.shields.io/pypi/pyversions/floe-guard.svg)](https://pypi.org/project/floe-guard/)
6+
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
7+
38
**A local budget guardrail for AI agents.** It hard-stops your agent *before its
49
next LLM call* when it would cross a spend ceiling — so a runaway loop dies at
510
$0.10 instead of $4,000. No account, no signup, no network. Runs in your process.
@@ -26,8 +31,9 @@ BUDGET EXCEEDED — call blocked
2631
The next call would cross your budget; floe-guard stopped your agent before it ran.
2732
```
2833

29-
_Animated demo coming — run `python examples/runaway_loop.py` to watch it stop a loop live._
30-
<!-- TODO: record docs/stop-the-loop.gif and restore the embed: ![stop the loop](docs/stop-the-loop.gif) -->
34+
![floe-guard hard-stopping a runaway loop before it crosses a $0.10 ceiling](docs/stop-the-loop.gif)
35+
36+
_Run it yourself: `python examples/runaway_loop.py` — no API key, no account, no network._
3137

3238
## See it stop a loop (no API key needed)
3339

@@ -138,8 +144,18 @@ Set `FLOE_API_KEY` and floe-guard exposes a hook to delegate enforcement to
138144
hosted Floe (see [`src/floe_guard/hosted.py`](src/floe_guard/hosted.py) — wiring
139145
the live endpoint is in progress; the local guard is fully functional today).
140146

141-
**[dev-dashboard.floelabs.xyz](https://dev-dashboard.floelabs.xyz)** ·
142-
**[floelabs.xyz](https://floelabs.xyz)**
147+
**[dev-dashboard.floelabs.xyz](https://dev-dashboard.floelabs.xyz/?utm_source=floe-guard&utm_medium=readme&utm_campaign=oss)** ·
148+
**[floelabs.xyz](https://floelabs.xyz/?utm_source=floe-guard&utm_medium=readme&utm_campaign=oss)**
149+
150+
## Built with floe-guard
151+
152+
Using floe-guard in your project? Add the badge so others find it:
153+
154+
[![guarded by floe-guard](https://img.shields.io/badge/guarded%20by-floe--guard-2f81f7.svg)](https://github.com/Floe-Labs/floe-guard)
155+
156+
```markdown
157+
[![guarded by floe-guard](https://img.shields.io/badge/guarded%20by-floe--guard-2f81f7.svg)](https://github.com/Floe-Labs/floe-guard)
158+
```
143159

144160
## Development
145161

docs/INSTRUMENTATION.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Instrumentation
2+
3+
How we measure floe-guard's adoption — and the one thing we deliberately *don't* do.
4+
5+
## No runtime telemetry (deliberate)
6+
7+
floe-guard does **not** phone home. It sends no usage events, no install pings, no
8+
identifiers — nothing leaves your process at runtime except the calls *you* make
9+
(e.g. `hosted_remaining_usd()`, only when you set `FLOE_API_KEY`).
10+
11+
This is a choice, not an oversight. A guardrail's whole value is trust: a library
12+
that silently exfiltrates usage from people's agents is the opposite of a tool you
13+
hand a budget to. Runtime telemetry in OSS dev tools reliably triggers backlash
14+
and forks. So we measure adoption only from **public, outside-in signals** — the
15+
ones below — and accept that they're coarser than telemetry. That trade is correct
16+
for this project.
17+
18+
## The signals we track
19+
20+
| Runbook KPI | How it's measured | Source |
21+
|---|---|---|
22+
| Star velocity | GitHub stargazers over time | `scripts/metrics.py` → GitHub API |
23+
| Fork count | GitHub forks | `scripts/metrics.py` |
24+
| Install velocity (`pip install`) | PyPI download counts (day/week/month) | `scripts/metrics.py` → pypistats.org |
25+
| README reach | repo views & unique clones | `scripts/metrics.py` (needs `GITHUB_TOKEN`) |
26+
| README → site click-through | UTM params on outbound links | Floe web analytics |
27+
| Install → hosted signup | UTM `source=floe-guard` lands in the signup funnel | Floe web analytics |
28+
| "Built with floe-guard" usage | badge backlinks | GitHub code/badge search |
29+
30+
### Snapshot script
31+
32+
```bash
33+
python scripts/metrics.py # stars, forks, PyPI downloads
34+
GITHUB_TOKEN=ghp_xxx python scripts/metrics.py # + repo views/clones
35+
```
36+
37+
It prints real numbers only — if a source is unreachable it says so rather than
38+
guessing. Run it on a schedule and diff snapshots to get *velocity* (the runbook's
39+
actual target), not just totals.
40+
41+
### UTM scheme
42+
43+
Outbound links in the README carry:
44+
45+
```
46+
?utm_source=floe-guard&utm_medium=readme&utm_campaign=oss
47+
```
48+
49+
So clicks from this repo into `dev-dashboard.floelabs.xyz` / `floelabs.xyz` are
50+
attributable in Floe's web analytics, and any session that converts to a hosted
51+
signup keeps `utm_source=floe-guard` — that's the **install → signup** handoff the
52+
runbook calls the critical metric. The conversion side lives in the Floe web app's
53+
funnel, not in this repo.
54+
55+
### "Built with floe-guard" badge
56+
57+
Downstream projects can advertise usage with the badge in the README's *Built with
58+
floe-guard* section. Each embed is a backlink — searchable on GitHub — and is the
59+
honest version of a "powered by" usage signal (opt-in, public, no tracking).
60+
61+
## What's still manual
62+
63+
- Posting the launch content and seeding (the runbook's Week-1/2 GTM motion).
64+
- Wiring the UTM-tagged sessions into a dashboard on the Floe web side.

docs/stop-the-loop.gif

110 KB
Loading

docs/stop-the-loop.gif.PLACEHOLDER.txt

Lines changed: 0 additions & 6 deletions
This file was deleted.

scripts/metrics.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"""Pull floe-guard's public adoption metrics into a single snapshot.
2+
3+
No dependencies — stdlib only. Reads public APIs:
4+
- GitHub repo stats (stars, forks, watchers) — no auth needed
5+
- GitHub traffic (views, clones) — needs a token with push access
6+
- PyPI downloads (last day / week / month) — no auth needed (pypistats.org)
7+
8+
Usage:
9+
python scripts/metrics.py
10+
GITHUB_TOKEN=ghp_xxx python scripts/metrics.py # also fetch traffic (views/clones)
11+
12+
These are the runbook KPIs (star velocity, forks, install velocity). Run it on a
13+
schedule and diff snapshots to get velocity. It only reports real numbers — if a
14+
source is unreachable it says so rather than guessing.
15+
"""
16+
from __future__ import annotations
17+
18+
import json
19+
import os
20+
import urllib.error
21+
import urllib.request
22+
23+
REPO = "Floe-Labs/floe-guard"
24+
PACKAGE = "floe-guard"
25+
26+
27+
def _get(url: str, headers: dict[str, str] | None = None, timeout: float = 15.0):
28+
req = urllib.request.Request(url, headers=headers or {})
29+
with urllib.request.urlopen(req, timeout=timeout) as resp: # noqa: S310 (trusted hosts)
30+
return json.loads(resp.read().decode())
31+
32+
33+
def github_repo() -> dict[str, object]:
34+
try:
35+
d = _get(
36+
f"https://api.github.com/repos/{REPO}",
37+
headers={"Accept": "application/vnd.github+json", "User-Agent": "floe-guard-metrics"},
38+
)
39+
return {
40+
"stars": d.get("stargazers_count"),
41+
"forks": d.get("forks_count"),
42+
"watchers": d.get("subscribers_count"),
43+
"open_issues": d.get("open_issues_count"),
44+
}
45+
except (urllib.error.URLError, ValueError) as e:
46+
return {"error": f"github repo stats unavailable: {e}"}
47+
48+
49+
def github_traffic() -> dict[str, object]:
50+
token = os.environ.get("GITHUB_TOKEN", "").strip()
51+
if not token:
52+
return {"note": "set GITHUB_TOKEN (push access) to fetch views/clones"}
53+
hdr = {
54+
"Authorization": f"Bearer {token}",
55+
"Accept": "application/vnd.github+json",
56+
"User-Agent": "floe-guard-metrics",
57+
}
58+
out: dict[str, object] = {}
59+
for kind in ("views", "clones"):
60+
try:
61+
d = _get(f"https://api.github.com/repos/{REPO}/traffic/{kind}", headers=hdr)
62+
out[kind] = {"count": d.get("count"), "uniques": d.get("uniques")}
63+
except (urllib.error.URLError, ValueError) as e:
64+
out[kind] = {"error": str(e)}
65+
return out
66+
67+
68+
def pypi_downloads() -> dict[str, object]:
69+
try:
70+
d = _get(
71+
f"https://pypistats.org/api/packages/{PACKAGE}/recent",
72+
headers={"User-Agent": "floe-guard-metrics"},
73+
)
74+
return d.get("data", {"error": "no data field"})
75+
except (urllib.error.URLError, ValueError) as e:
76+
return {"error": f"pypistats unavailable (new packages take ~1 day to appear): {e}"}
77+
78+
79+
def main() -> None:
80+
snapshot = {
81+
"repo": REPO,
82+
"package": PACKAGE,
83+
"github": github_repo(),
84+
"traffic": github_traffic(),
85+
"pypi_downloads_recent": pypi_downloads(),
86+
}
87+
print(json.dumps(snapshot, indent=2))
88+
89+
90+
if __name__ == "__main__":
91+
main()

0 commit comments

Comments
 (0)