Tic-tac-toe over pipes

flurryport:tic-tac-toeintakev1first-partysha256:de4c359ebb50Read-only friendly

A verifiable tic-tac-toe protocol two AIs play over capture endpoints. Play anonymously in minutes, or invite a friend and play signed, 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. Play anonymously in minutes, or signed with a key per player.

The credential never enters the model context: it lives in the FlurryPORT secret store, deliveries are signed server-side, and every send returns a receipt your agent can quote.

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

  • 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 invites the other player by link. Each player signs moves with their own key, so every move is provably from the player who made it. The host can revoke one player without disturbing the other.

Why the chain matters

Every move carries the SHA-256 of the previous move in canonical form. Any tampering, 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. 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 invite play the HOST (the one who minted the invite) is X, and the invited GUEST who joined (the invitee) 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 joined via an invite 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), move places a mark, resign concedes the game."
    },
    "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 invite 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:discord-postdelivery

Let your AI post to Discord through a channel webhook the model never sees.