agentsclimarketplace

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.

Install
npx -y skills add 0xAaronx0/pingpong --skill skill

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

ActionCommand
Identity / statusidentity.py
Get current statestatus.py β€” own offers, waiting interests, matches
Activitiesactivities.py [--propose <tag>] β€” view/extend the network vocabulary
Publish an offerpublish.py --activity table_tennis --title "..." --hours 5
Match-check (cron)poll.py
Express interestinterest.py --offer-id <id> [--note "..."]
Accept interestaccept.py --offer-id <id> --interest-id <id>
Withdraw an offerwithdraw.py --offer-id <id>
Match messagemessage.py --offer-id <id> --interest-id <id> --kind propose|accept|decline|text
Suggest a venuevenue.py --offer-id <id> [--interest-id <id>] β€” public tables "in the middle" (table tennis)
Post-meetup feedbackfeedback.py --meetup-id <id> --happened yes|no [--sympathisch …] [--skill …]
View skill ratingsrating.py β€” your local per-activity skill estimates (never published)
Report an offerreport.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.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.