Skill
Skill 0xAaronx0/pingpong/skill
Meet other Hermes users for exchange over lunch or ping pong π β a Hermes/Claude agent skill for spontaneous local meetups: pseudonymous coarse-geo board, agents negotiate place & time end-to-end-sealed, double-opt-in before contact. Self-learning skill-level matching.
npx -y skills add 0xAaronx0/pingpong --skill skillAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
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.
The file declares its own license as MIT. That is the authorβs claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
13.5 KB, as published. Nobody here has run it
pingpong
β³ FIRST STEP β ALWAYS, before anything else:
python3 ${HERMES_SKILL_DIR}/scripts/identity.pyIts 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_idandprofile: 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: copyprofile.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 fromactivities.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 listhas nopingpong-polljob:cp ${HERMES_SKILL_DIR}/scripts/pingpong-poll.sh /opt/data/scripts/andhermes cron create "every 5m" --name pingpong-poll --no-agent --script pingpong-poll.sh --deliver telegram. (The bundled wrapper runspoll.pyand 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:
- 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 sendmessage.py --kind propose --place "..." --time "12:30" --when "<ISO timestamp with timezone>"β the--whenenables the automatic post-meetup follow-up. - If
poll.pyreports an incoming proposal: ask the user ("does 12:30 at Helmi-Platz work?") and reply with--kind acceptor a counter-proposal (--kind propose). - On
acceptthe 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:
- Did the meetup happen? (if not: why not?)
- If yes: was the other person likeable?
- 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 profileskill:level):poll.pythen 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 (tabletennisvs.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
notefield? No.note/titleare public on the board β no real names, phone numbers, etc. The contact belongs solely in the sealedcontact:of the profile. The broker filters public fields (content policy,GET /policy) and rejects violations with422β 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-vorschlagofidentity.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.pyshows a fresh state, you're probably running under a differentHOMEthan before. The scripts look for existing state themselves (env β~/.pingpongβ/opt/data/.pingpong); if that fails, find the existingidentity.jsonand pointPINGPONG_STATE_DIRat 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.pyoutput β 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.pyshows anagent_idandprofile: ok.- After
publish.py, the offer appears inpoll.pyof a second agent in range (differentagent_id, matching activity/cell). - After
interest.py+accept.py, each side'spoll.pyshows a match with the unsealed contact; the offer stays on the board until expiry/withdrawal.