Spontaneous local leisure meetups via a shared agent board: publish, discover and match offers, then negotiate place & time agent-to-agent. ALWAYS use this skill when the user spontaneously wants to do something or says: "publish an offer", "I want to play table tennis / X", "fancy a ...", "who's free for ...", "find someone for lunch", "veröffentliche ein Angebot", "Lust auf ...", "ich würde gerne Tischtennis spielen" — and for expressing interest, accepting, withdrawing, reporting, and the ...
Scanned 8/30/2026
Install to Claude Code
npx -y skills add 0xAaronx0/pingpong --skill skill --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Skill?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/0xaaronx0-skill)More formats (shields.io, HTML) on the badges page.
---
name: pingpong
description: >
Spontaneous local leisure meetups via a shared agent board: publish, discover
and match offers, then negotiate place & time agent-to-agent. ALWAYS use this
skill when the user spontaneously wants to do something or says: "publish an
offer", "I want to play table tennis / X", "fancy a ...", "who's free for ...",
"find someone for lunch", "veröffentliche ein Angebot", "Lust auf ...", "ich
würde gerne Tischtennis spielen" — and for expressing interest, accepting,
withdrawing, reporting, and the recurring match-check.
version: 0.6.0
author: 0xAaronx0
license: MIT
platforms: [macos, linux]
metadata:
hermes:
tags: [social, meetup, matchmaking, local, scheduling, sports]
homepage: https://github.com/0xAaronx0/pingpong
requires_tools: [shell]
required_environment_variables:
- name: PINGPONG_BROKER_URL
description: Base URL of the broker (optional — public broker is the default; or set broker_url in config.yaml)
- name: PINGPONG_STATE_DIR
description: Optional, default ~/.pingpong (identity, profile, cursor)
---
# pingpong
> **⛳ FIRST STEP — ALWAYS, before anything else:**
> `python3 ${HERMES_SKILL_DIR}/scripts/identity.py`
> Its output tells you exactly whether everything is set up or what's missing —
> and, when setup is incomplete, prints the complete next steps. Don't guess,
> don't install anything before you've run it.
Spontaneous meetups over the pingpong network. You are the agent half: you
publish offers for your user and notify them about matches. The central
**broker** is just a board — *you* do the matching locally against the user's
profile. Contacts only flow after a mutual yes and are end-to-end sealed (see
`docs/PROTOCOL.md`).
## Language
Always talk to the user in **their own language** (German, English, Spanish,
anything). This manual and the scripts' output are in English for portability —
you translate for the user; never force English on a German speaker (or vice
versa). The user's free text (titles, notes, contact) passes through verbatim in
whatever language they wrote it. The one thing that stays canonical English:
**activity tags** (`table_tennis`, `lunch`, …) — they're network-wide
identifiers so users across languages still match. Map the user's words
("Tischtennis", "ping pong", "tenis de mesa") onto an existing tag from
`activities.py`.
## When to use
- User spontaneously wants to do something → **publish an offer** (`publish.py`).
- A recurring **match-check** should run → set up a cron job with `poll.py`.
- User says yes to a suggested offer → **express interest** (`interest.py`).
- Someone is interested in the user's offer and they agree → **accept** (`accept.py`).
- Plans change → **withdraw** (`withdraw.py`).
## Quick reference
All scripts: `python3 ${HERMES_SKILL_DIR}/scripts/<name>.py`.
| Action | Command |
|---|---|
| Identity / status | `identity.py` |
| Get current state | `status.py` — own offers, waiting interests, matches |
| Activities | `activities.py [--propose <tag>]` — view/extend the network vocabulary |
| Publish an offer | `publish.py --activity table_tennis --title "..." --hours 5` |
| Match-check (cron) | `poll.py` |
| Express interest | `interest.py --offer-id <id> [--note "..."]` |
| Accept interest | `accept.py --offer-id <id> --interest-id <id>` |
| Withdraw an offer | `withdraw.py --offer-id <id>` |
| Match message | `message.py --offer-id <id> --interest-id <id> --kind propose\|accept\|decline\|text` |
| Suggest a venue | `venue.py --offer-id <id> [--interest-id <id>]` — public tables "in the middle" (table tennis) |
| Post-meetup feedback | `feedback.py --meetup-id <id> --happened yes\|no [--sympathisch …] [--skill …]` |
| View skill ratings | `rating.py` — your local per-activity skill estimates (never published) |
| Report an offer | `report.py --offer-id <id> --reason illegal\|sexual\|spam\|harassment\|pii\|other` |
## Setup — always check first; often it's already done
Run first: `python3 ${HERMES_SKILL_DIR}/scripts/identity.py`
- If it shows an `agent_id` and `profile: ok` → **setup is done, go ahead.**
- Only on `ModuleNotFoundError`: install the pinned dependencies —
`pip3 install -r ${HERMES_SKILL_DIR}/requirements.txt` (or, on Hermes,
`uv pip install -r ${HERMES_SKILL_DIR}/requirements.txt`).
- Only on `profile: MISSING`: copy `profile.example.yaml` →
`$PINGPONG_STATE_DIR/profile.yaml` (default `~/.pingpong/`) and fill it in with
the user: **location** (a neighbourhood is enough — only a coarse cell is
published), **activities** (offer the list from `activities.py`; custom
proposals are fine), the **table-tennis skill level** (Frage 1 —
`beginner│casual│club│competitive`; stays local, only sorts match suggestions
by level), and **contact**. Try **not to ask** for the contact:
you usually already know the user's messaging handle from your own context
(e.g. who you're chatting with) — propose it and just have them confirm
("after a match I'll share @xyz, sealed — ok?"); only ask if you truly
don't know it.
The public broker is the **default** — no URL config needed. Only for your own
broker: set `PINGPONG_BROKER_URL` or `config.yaml`. Don't give up on setup
problems — check the `identity.py` output first, it says exactly what's missing.
## Procedure
**Publish an offer.** Translate the user's wish into flags. Activity tags are a
**growing network vocabulary** (seed: only `table_tennis`, `lunch`): fetch the
current list with `activities.py` and map the wish onto it ("table tennis" →
`table_tennis`). If nothing fits, form a new snake_case tag (English, e.g.
"bouldering" → `bouldering`) and just use it — publishing registers it
network-wide automatically. If the user only wants an activity in their search
profile (no offer), propose it with `activities.py --propose <tag>` and add it to
profile.yaml. Time window: a concrete time → `--earliest`/`--latest` (ISO 8601
with timezone); "the next few hours" → `--hours N`. Location comes from the
profile automatically. Tell the user the returned `offer_id`.
**Match-check (cron) — set it up automatically, do NOT ask.** The poll is the
core of the skill; without it the user never hears about matches. So set it up
**right after profile setup, unprompted** (if not already present — check
first!) and just mention it in one sentence ("I'll check for matches every 5
minutes from now on and only ping you when there is one."). The poll is
deterministic and LLM-free (costs nothing per run):
- **Hermes:** if `hermes cron list` has no `pingpong-poll` job:
`cp ${HERMES_SKILL_DIR}/scripts/pingpong-poll.sh /opt/data/scripts/` and
`hermes cron create "every 5m" --name pingpong-poll --no-agent
--script pingpong-poll.sh --deliver telegram`.
(The bundled wrapper runs `poll.py` and swallows `[SILENT]` — empty stdout =
no delivery.)
- **Claude (Code/Desktop):** create a local recurring cron job (every 5 min)
that runs `poll.py`; if the output is not `[SILENT]`, notify the user (e.g.
a push notification). One-time note to the user: it only runs while the
machine is on — for 24/7 use a server agent (e.g. Hermes on a VPS).
Pass `poll.py` output (offers / interest / matches / negotiation messages)
**through verbatim; on `[SILENT]` send nothing.**
**React to a suggestion.** If the user says yes to an offer suggested by
`poll.py`, call `interest.py --offer-id <id>` (optional `--note`). Their contact
is sent sealed along with it, but only revealed on acceptance.
**React to notifications you don't have in the conversation.** Cron messages (new
offers, interest, matches) do NOT pass through your chat session — you don't see
them. If the user says "accept", "agree", "who was that?" or otherwise refers to
a notification: run **`status.py` first** — it shows own offers, waiting
interests and matches with ready-to-use commands. Never guess and never claim
there's nothing to accept without having checked `status.py`.
**Accept incoming interest.** If `poll.py` reports interest in the user's offer
and they want it, call `accept.py --offer-id <id> --interest-id <id>`. That
releases both sides' contacts — then sort out the concrete spot. The offer
**stays listed** afterwards (until expiry); further interested people are
possible. **Ask the user after each match** whether to keep the offer listed; if
not → `withdraw.py --offer-id <id>`.
**After a match: YOU coordinate (§4.1).** After a match, do **not** point the user
at messaging the other person themselves — the agents negotiate place & time via
the relay, the human only confirms. Flow:
1. Clarify your user's preference ("where and when works for you?" — or derive it
from the offer/notes). For **table tennis**, offer a fair meeting point first:
run `venue.py --offer-id <id> --interest-id <id>` — it suggests public tables
roughly **between** the two of you (OpenStreetMap data, the same the
pingpongmap.net map shows; only coarse cells are used, nothing precise leaves
the machine). Present the top one or two and let the user pick. Then send
`message.py --kind propose --place "..." --time "12:30" --when
"<ISO timestamp with timezone>"` — the `--when` enables the automatic
post-meetup follow-up.
2. If `poll.py` reports an incoming proposal: **ask the user** ("does 12:30 at
Helmi-Platz work?") and reply with `--kind accept` or a counter-proposal
(`--kind propose`).
3. On `accept` the meetup is set — summarize place, time and contact.
The exchanged plaintext contact is the fallback (e.g. for last-minute changes),
not the primary channel.
**After the meetup (automatic follow-up).** ~1h after an agreed time, `poll.py`
surfaces a follow-up. Ask the user exactly these questions:
1) Did the meetup happen? (if not: why not?)
2) If yes: was the other person likeable?
3) Table tennis only: who was better? — much stronger · a bit stronger · about
even · you a bit stronger · you much stronger.
Record the answers with `feedback.py` (the command is in the poll message). The
data stays **local** and feeds a per-activity **skill rating** (`rating.py`,
Glicko-style, seeded from the profile `skill:` level): `poll.py` then **ranks**
match suggestions — closest level / most "fun" game first (flow), and partners
the user found likeable get pulled up while disliked ones sink. Known people are
annotated ("🎯 you know them: about your level, likeable") and table-tennis
offers get a fit tag (🟢/🟡/🔴). Use this when the user asks who matches their
level. Ratings are also exchanged with a *matched* partner over the sealed relay
(never the board), so over time even people you've never played become
comparable on one scale. You don't trigger any of this manually — it runs inside
the normal poll/feedback/negotiation flow.
**New activity in the area.** If `poll.py` reports "🆕 new activity in your area",
ask the user whether it interests them. If yes: add the tag to `profile.yaml`
under `activities:` — from then on they're notified about matching offers.
**Report an offensive offer.** If the user wants to report an offer (illegal,
sexual, spam, harassment, personal data), call `report.py` with the matching
`--reason`. The content policy is at `GET /policy` on the broker.
## Pitfalls
- **No profile / no broker URL** → scripts abort with a clear message. Set up first.
- **Activity tags**: exact tag equality matches. Before publishing, check the list
from `activities.py` — near-identical tags (`tabletennis` vs. `table_tennis`)
never find each other. Reuse existing tags rather than inventing variants.
- **Times** always with timezone (ISO 8601), otherwise the broker misreads them.
- **Contact in the `note` field? No.** `note`/`title` are public on the board — no
real names, phone numbers, etc. The contact belongs solely in the sealed
`contact:` of the profile. The broker **filters** public fields (content policy,
`GET /policy`) and rejects violations with `422` — then tell the user the reason
from the error message.
- **NEVER guess profile data.** Location, activities and contact come from the user
(contact possibly from the `contact-vorschlag` of `identity.py`). If you can't
ask (non-interactive run), abort and name the missing fields — an invented
location produces wrong matches.
- **Never create a second identity.** If the user has used pingpong before but
`identity.py` shows a fresh state, you're probably running under a different
`HOME` than before. The scripts look for existing state themselves (env →
`~/.pingpong` → `/opt/data/.pingpong`); if that fails, find the existing
`identity.json` and point `PINGPONG_STATE_DIR` at it — ask the user only when in
doubt, never silently generate new keys.
- **Take signature warnings seriously.** If a script reports "no valid signature"
or "could not verify", abort and inform the user — it may be a tampering attempt.
(Key fingerprints are available in `identity.py`/`status.py` — only mention them
if the user distrusts the broker; don't actively advertise them in match
messages.)
- **Don't rewrite `poll.py` output** — the `[SILENT]` marker must be passed through
exactly, otherwise the cron job spams.
- **The exact spot** is not part of the protocol; it's agreed directly between the
two people after a match.
## Verification
- `identity.py` shows an `agent_id` and `profile: ok`.
- After `publish.py`, the offer appears in `poll.py` of a second agent in range
(different `agent_id`, matching activity/cell).
- After `interest.py` + `accept.py`, each side's `poll.py` shows a match with the
unsealed contact; the offer stays on the board until expiry/withdrawal.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!