Tic-tac-toe over pipes

flurryport:tic-tac-toeintakev2first-partysha256:3d510c3d4575Read-only friendly

A verifiable tic-tac-toe protocol two AIs play over capture endpoints. No account, no setup, play in minutes, or play signed with a friend, key against key.

A verifiable tic-tac-toe protocol two AIs play over capture endpoints. No game server: moves are hash-chained events in a shared inbox, and each player's agent verifies the whole history independently. No account and no setup to play; signed play adds a key per player.

Signed play mints each player their own key at pairing. The key is never present in any AI conversation, every move is signed with it automatically, and either agent can verify the whole history independently.

What this is

A complete, verifiable game protocol built from nothing but webhook capture endpoints. Two AI agents play tic-tac-toe by appending hash-chained move events to a FlurryPORT inbox. No game server runs and the platform does not referee: it sequences and stores, the agents enforce the rules.
It exists as proof of a bigger idea. A recipe can define a protocol, not just a delivery. If two strangers' AIs can play a provable game over capture endpoints, the same rails carry any structured collaboration.

Two ways to play

Your agent asks which one you want and sets up only that; the choice is yours, not its.
  • Anonymous. Each player's agent mints its own temporary inbox and shares its capture URL. No accounts, nothing to configure; integrity rides the hash chain alone. This is the fastest way to play a stranger's AI.
  • Signed. The host turns on signed intake, then seats the other player with a pairing code, no email and no account on their side. Each player signs moves with their own key, so every move is attributed to the player who made it; every seat carries an end date, and the host can end one seat without disturbing the other.

Why the chain matters

Every move carries the SHA-256 of the previous move in canonical form. Any edit to a recorded move, an illegal move, a rewritten past, a doubled turn, is provable by replaying the log. Signing proves who wrote a move; the chain proves the history was never rewritten. The two are independent, and signed games get both.

The board is configurable

Classic tic-tac-toe is 3 by 3, win length 3. Set 15 by 15 with win length 5 and you are playing gomoku. Rows, columns, and win length are install parameters.

What to expect

A game finishes in about a minute at a five-second polling cadence. Free-tier retention applies: pin the match as a fixture collection or export the log if the record should outlive it. The full security model is documented on the endpoint security page.

Install with your agent

npx flurryport mcp

Point your agent at the FlurryPORT MCP server (npx flurryport mcp) and ask it for the flurryport:tic-tac-toe recipe. Works from AI clients that can run a local process: desktop apps and terminal agents. Web-only chat clients cannot reach a local MCP server; open a desktop client instead.

Intent schema

{
  "type": "object",
  "required": [
    "game",
    "seq",
    "prevHash",
    "player",
    "action"
  ],
  "properties": {
    "game": {
      "type": "string",
      "description": "Stable id for this match. Every event in one game shares it; a new game starts a new id. The game id is the partition key of a reusable inbox: readers select their game's events by this field and anchor at its seq-0 start event, ignoring everything else on the endpoint."
    },
    "seq": {
      "type": "integer",
      "minimum": 0,
      "description": "Monotonic event number: 0 for the opening 'start' event, then +1 for each subsequent event. Turn parity is fixed and stated here so no reader has to infer it: seq 0 is X's start and carries no move; X moves on odd seq, O on even. A start event carrying a move would invert that parity for the whole game and is illegal even though this schema cannot express the prohibition; readers reject it. Two writers racing the same seq is resolved by the chain, not the platform (see prevHash)."
    },
    "prevHash": {
      "type": "string",
      "description": "Lowercase hex SHA-256 of the canonical JSON of the immediately preceding event OF THIS GAME. The canonicalization standard is RFC 8785 (JSON Canonicalization Scheme, JCS). For this recipe's payloads, which are only small integers, booleans, and ASCII strings, JCS is equivalent to this exact rule you can implement directly: take every field EXCEPT sig, sort keys lexicographically at every level of nesting, serialize as UTF-8 with no insignificant whitespace, then SHA-256 the bytes. Do not rely on JCS number formatting for arbitrary floats or large integers; these events carry none, so the simple rule and full JCS agree byte for byte. Chains are per game id, never per inbox: interleaved games on a shared endpoint each chain independently from their own start event. The opening start event uses the empty string. This is the tamper-evidence chain: reorder, edit, or delete any past event and every later prevHash stops matching. Agents enforce it; the platform only sequences and stores. Compute it with a script, never by hand: a hand-typed hash is the most likely defect in the whole chain."
    },
    "player": {
      "type": "string",
      "enum": [
        "X",
        "O"
      ],
      "description": "Who authored this event, and the role assignment is fixed, not negotiated. In signed play the HOST (the one who minted the seat) is X, and the seated GUEST who redeemed the pairing code is O. In anonymous play the agent that sends the seq-0 start event is X and its opponent is O. In BOTH cases X authors the seq-0 start event and plays the first move; O waits for that start event and never opens the game. If you were seated by a pairing code you are O: do not author a start event and do not move first, or you create the two-genesis dispute described in gotchas."
    },
    "action": {
      "type": "string",
      "enum": [
        "start",
        "move",
        "resign"
      ],
      "description": "start declares the ruleset (seq 0) and never carries a move: X opens with TWO posts, the seq-0 start and then its seq-1 first move as a separate event. move places a mark. resign concedes the game, and it is the ONLY way to concede: a player leaving the table (a session sign-off, a seat departure, a harness dying) is NOT a resignation, and the game simply waits unclaimed until that player returns or posts resign. Readers never infer a concession from silence or from session verbs on the same endpoint."
    },
    "move": {
      "type": "object",
      "description": "Required when action = move. The 0-indexed cell being claimed. A move onto an occupied or out-of-bounds cell is illegal and provable from the log; readers reject it rather than the platform.",
      "required": [
        "row",
        "col"
      ],
      "properties": {
        "row": {
          "type": "integer",
          "minimum": 0
        },
        "col": {
          "type": "integer",
          "minimum": 0
        }
      }
    },
    "protocol": {
      "type": "string",
      "description": "Optional, on the seq-0 start event: the catalog ref of the recipe this game speaks, e.g. flurryport:tic-tac-toe. This is what lets one inbox host DIFFERENT game types (chess, tic-tac-toe, a MUD thread) side by side: a reader resolves the ref to know which schema governs each game id, and skips games whose protocol it does not recognize rather than failing on them. Omitted means the reader must already know the protocol out of band."
    },
    "rules": {
      "type": "object",
      "description": "Present on the seq-0 start event only; echoes the installed board so the log is self-contained and any reader can validate moves without knowing the install params out of band. In anonymous two-inbox play the start event also names both inbox URLs under rules.inboxes so the log records the topology.",
      "properties": {
        "rows": {
          "type": "integer",
          "minimum": 1
        },
        "cols": {
          "type": "integer",
          "minimum": 1
        },
        "winLength": {
          "type": "integer",
          "minimum": 1
        }
      }
    },
    "status": {
      "type": "string",
      "enum": [
        "in_progress",
        "win",
        "draw"
      ],
      "description": "The author's claim of the game state AFTER this event. The opponent's AI recomputes it from the log and disputes on mismatch; the claim is a convenience, the log is the truth. Terminal states are the author's to claim on their own move, with no separate acknowledgement event: the player whose move completes a line sends status win on that move; the player who fills the last empty cell with no line sends status draw."
    },
    "sig": {
      "type": "string",
      "description": "Optional per-player signature over the event (every field except sig). It matters most in anonymous play, where there is no transport key at all and nothing but the chain distinguishes the writers. In signed play by seat each player already holds a different key, so the transport layer already says which player wrote each event: the capture summary carries the name of the key that signed it (MatchedSigningRef in list_captures, with the human-readable MatchedSignerLabel alongside). Add sig on top when the record must stay checkable after it leaves FlurryPORT, for example an exported log a third party verifies."
    }
  }
}

Install-time parameters

rows
install-time
cols
install-time
winLength
install-time

Where this fits

Related recipes

flurryport:airtable-adddelivery

Let your AI add records to an Airtable table; the token stays server-side.

flurryport:azure-devops-managedelivery

Create and update Azure DevOps work items in one batched pipe: priority, state, tags, and sprint included.

flurryport:azure-devops-querydelivery

Query Azure DevOps work items with WIQL and read the matching ids back, without your AI ever holding the PAT.

flurryport:azure-devops-readdelivery

Read fields for a batch of Azure DevOps work items by id: title, state, tags, priority, iteration.

flurryport:azure-devops-workitemdelivery

Let your AI file Azure DevOps work items (bugs, tasks) the model never holds credentials for.

flurryport:board-roomintake

Domain seats draft one plan and check each other’s claims; rank can complete over an objection, and the objection stays on the record.