> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pome.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Get started

> Run a digital twin on your own machine, let your coding agent do a real piece of work in it, and read back the twin's record of every call. No account, no API key, nothing graded.

<Info>
  <Icon icon="clock" /> **About five minutes, no account, and nothing graded.**
</Info>

Three steps, and they are the same three every time you use Pome: start a twin,
let your agent work in it, then read what the twin wrote down. It all runs on
your own machine.
[Introduction](/introduction) defines the words this page uses.

## Before you start

* **Node 24+** for `npx`, plus `curl` and `jq` for the transcripts below.
* **Your own coding agent** — anything that runs a shell command and reads JSON.

That is the whole list: **no Pome account, no vendor account, no
`ANTHROPIC_API_KEY`, and no model inference paid for by Pome.** Your own agent
is both the operator and the actor here.

Every output block below is pasted from a real run against a local twin. Two
edits are disclosed: bearer tokens are cut short, and the start-up transcripts
drop the closing health-check and Ctrl-C lines.

<Note>
  **Nothing on this page is graded.** No task file, no criteria, no score — and no
  agent eval is charged, because an eval is only ever burned when a run is graded.
  A sandbox you start, drive and stop costs you nothing. Grading appears exactly
  once in this curriculum, at the
  [graded capstone](/quickstart/coding-agent), where the agent under test
  is sealed off from the criteria that judge it.
</Note>

<Accordion title="In a hurry? Hand your agent the whole thing" icon="forward">
  One prompt, and your agent does every step below: starts the twin, works in it,
  and reads back what the twin recorded. The three steps after this are the same
  run, one beat at a time, with you at the keyboard.

  ```text wrap theme={"dark"}
  Set up a Pome GitHub twin in this folder, do one real piece of
  work in it, then show me what the twin recorded.

  Show me this plan first and wait for my go-ahead. Keep every
  command plain text; never pipe a remote script into a shell.

  1. Start the twin IN THE BACKGROUND, in this folder:
       npx @pome-sh/cli@latest twin start github
     Do not wait on it -- it is a foreground server and will not
     exit on its own. If the port is taken it says so; start it on
     another with --port. It prints an unauthenticated health-check
     command: run that to confirm it is up.
     Starting it writes .pome/twin-status.json here. Take rest_url
     and auth_token from that file for every call below, rather
     than assuming a port.

  2. Read the world before you touch it:
       GET <rest_url>/repos/acme/api/issues
     Tell me what is open and how it is labelled.

  3. Triage that issue the way you would triage a real one,
     through the twin's own GitHub surface -- the same routes and
     payloads api.github.com takes:
       POST .../issues/<n>/comments   a repro summary drawn from
                                      the issue body, not invented
       POST .../issues/<n>/assignees  ["alice"]
     Do not open a new issue and do not close this one.

  4. Read the result back THROUGH THE TWIN, not from your memory
     of what you sent:
       GET .../issues/<n>
       GET .../issues/<n>/comments

  5. Show me the tape: GET <rest_url>/_pome/events
     Print those rows exactly as they come back, one line per
     call, BEFORE you say anything about them. Then tell me what
     they show.

  This is a twin, not an exam. Nothing here is scored and Pome is
  not evaluating you. Keep your own evaluator and observability
  setup exactly as it is. Leave the twin running when you finish.
  ```

  Swap `github` for `slack`, `stripe`, `gmail` or `linear` to get one of the
  other worlds; the work in beat 3 is then that twin's, and your agent can read
  what it holds before deciding.

  Your agent's wording will differ from anything printed on this page. The tape
  will not — so when it finishes, read that back yourself with the command under
  [Read the tape](#read-the-tape). That read is the only account of the run your
  agent did not write.
</Accordion>

<Steps titleSize="h3">
  <Step title="Start your twin">
    The twin runs in the foreground and prints the two values every later call
    needs: the URL it is listening on, and the bearer. Ctrl-C stops it.

    <Tabs>
      <Tab title="GitHub">
        ```bash theme={"dark"}
        npx @pome-sh/cli@latest twin start github
        ```

        ```text wrap theme={"dark"}
        Pome github twin listening at http://127.0.0.1:3333/s/standalone
        Seed: the github twin's default (pass --seed <path>, or write one with `pome twin new-seed github`).
        POME_GITHUB_REST_URL=http://127.0.0.1:3333/s/standalone
        POME_GITHUB_MCP_URL=http://127.0.0.1:3333/s/standalone/mcp
        POME_AUTH_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
        ```

        The world is one repository, `acme/api`, holding one open bug report — issue
        \#1, labelled `bug`, unassigned, no comments.
      </Tab>

      <Tab title="Slack">
        ```bash theme={"dark"}
        npx @pome-sh/cli@latest twin start slack --port 3334
        ```

        ```text wrap theme={"dark"}
        Pome slack twin listening at http://127.0.0.1:3334/s/standalone
        Seed: the slack twin's default (pass --seed <path>, or write one with `pome twin new-seed slack`).
        POME_SLACK_REST_URL=http://127.0.0.1:3334/s/standalone
        POME_SLACK_MCP_URL=http://127.0.0.1:3334/s/standalone/mcp
        POME_AUTH_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
        ```

        The world is two channels, `#general` and `#random`. `#general` holds a short
        morning exchange between Alice and Bob, and nobody has replied in-thread.
      </Tab>

      <Tab title="Stripe">
        ```bash theme={"dark"}
        npx @pome-sh/cli@latest twin start stripe --port 3335
        ```

        ```text wrap theme={"dark"}
        Pome stripe twin listening at http://127.0.0.1:3335/s/standalone
        Seed: the stripe twin's default (pass --seed <path>, or write one with `pome twin new-seed stripe`).
        POME_STRIPE_REST_URL=http://127.0.0.1:3335/s/standalone
        POME_STRIPE_MCP_URL=http://127.0.0.1:3335/s/standalone/mcp
        POME_AUTH_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
        ```

        The world starts **empty** — no customers, no charges, no refunds, and
        `balance.available` is `[]`. There is nothing seeded to refund, so you build
        the money first. That is the point of this one.
      </Tab>

      <Tab title="Gmail">
        ```bash theme={"dark"}
        npx @pome-sh/cli@latest twin start gmail --port 3336
        ```

        ```text wrap theme={"dark"}
        Pome gmail twin listening at http://127.0.0.1:3336/s/standalone
        Seed: the gmail twin's default (pass --seed <path>, or write one with `pome twin new-seed gmail`).
        POME_GMAIL_REST_URL=http://127.0.0.1:3336/s/standalone
        POME_GMAIL_MCP_URL=http://127.0.0.1:3336/s/standalone/mcp
        POME_AUTH_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
        POME_GMAIL_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
        ```

        The world is a five-message mailbox for `pome-agent@pome-twin.test`. Two
        threads are unread; `msg_support` is a customer asking why their production
        export is stuck.
      </Tab>

      <Tab title="Linear">
        ```bash theme={"dark"}
        npx @pome-sh/cli@latest twin start linear --port 3337
        ```

        ```text wrap theme={"dark"}
        Pome linear twin listening at http://127.0.0.1:3337/s/standalone
        Seed: the linear twin's default (pass --seed <path>, or write one with `pome twin new-seed linear`).
        POME_LINEAR_REST_URL=http://127.0.0.1:3337/s/standalone
        POME_LINEAR_MCP_URL=http://127.0.0.1:3337/s/standalone/mcp
        POME_AUTH_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
        POME_LINEAR_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
        ```

        The world is four issues on one team, one per workflow state — ENG-1 Backlog,
        ENG-2 Todo, ENG-3 In Progress, ENG-4 Done — and one seeded comment on each of
        the first three, so a read-back on ENG-2 returns two after your agent comments.
      </Tab>
    </Tabs>

    Starting it also writes `.pome/twin-status.json` in the folder you ran it from,
    holding that same URL and bearer as `rest_url` and `auth_token`. Everything
    below reads the twin's address out of that file rather than asking you to copy
    a 187-character token out of a wrapped terminal. It is one file per folder, so
    a second twin started here replaces it.

    <Warning>
      **One token opens a twin: `POME_AUTH_TOKEN`.** It is the bearer on every call,
      REST and MCP alike, and `twin start` printed it on the line after the URL. The
      `POME_GMAIL_TOKEN` and `POME_LINEAR_TOKEN` lines are that same token again under
      a twin-named alias, and a `github_pat_…` or `sk_test_…` served *inside* a twin is
      data in the world, not a way into it.
    </Warning>

    <Accordion title="If a call comes back 401 or 404" icon="triangle-alert">
      A local twin has no proxy in front of it, so a wrong bearer gets you the
      vendor's own refusal. GitHub answers `401 Bad credentials`, and that is the
      twin reproducing GitHub, not Pome reporting an error.

      Hosted is where that changes. `pome sandbox create` puts a routing proxy in
      front, and a bad bearer there comes back as an opaque

      `404 No twin pod for this session.` So a 404 on a sandbox you have just created
      is almost always the wrong bearer rather than a dead sandbox.
    </Accordion>
  </Step>

  <Step title="Paste this">
    Hand your coding agent the matching prompt as it is, from the folder you
    started the twin in. The prompts name `.pome/twin-status.json` rather than a
    `$POME_GITHUB_REST_URL`, because nothing expands a shell variable inside a chat
    message — a prompt that reads the file works whatever port the twin took.

    Every prompt has the same shape: read the world, do one real piece of work in
    it, then read the result back through the twin rather than from memory of what
    was sent. That last step is the one that matters, and it is the one agents
    skip.

    <Tabs>
      <Tab title="GitHub">
        ```text wrap theme={"dark"}
        The Pome GitHub twin is running in this folder. Take its URL
        and the bearer for every call from .pome/twin-status.json --
        rest_url and auth_token. Triage the bug already in it, then
        show me the recorded tape.

        Show me this plan first and wait for my go-ahead. Keep every
        command plain text; never pipe a remote script into a shell.

        1. Read the world before you touch it:
             GET <rest_url>/repos/acme/api/issues
           Tell me what is open and how it is labelled.

        2. Triage that issue the way you would triage a real one,
           through the twin's own GitHub surface -- the same routes and
           payloads api.github.com takes:
             POST .../issues/<n>/comments   a repro summary drawn from
                                            the issue body, not invented
             POST .../issues/<n>/assignees  ["alice"]
           Do not open a new issue and do not close this one.

        3. Read the result back THROUGH THE TWIN, not from your memory
           of what you sent:
             GET .../issues/<n>
             GET .../issues/<n>/comments

        4. Show me the tape, one line per call, in the order they
           happened: GET <rest_url>/_pome/events
           Print those rows exactly as they come back before you say
           anything about them.

        This is a twin, not an exam. Nothing here is scored and Pome is
        not evaluating you. Keep your own evaluator and observability
        setup exactly as it is. Leave the twin running when you finish.
        ```
      </Tab>

      <Tab title="Slack">
        ```text wrap theme={"dark"}
        The Pome Slack twin is running in this folder. Take its URL
        and the bearer for every call from .pome/twin-status.json --
        rest_url and auth_token. Answer the morning thread in it, then
        show me the recorded tape.

        Show me this plan first and wait for my go-ahead. Keep every
        command plain text; never pipe a remote script into a shell.

        1. Read the world before you touch it. The twin serves Slack's
           methods at the BARE path -- /conversations.list, not
           /api/conversations.list:
             GET <rest_url>/conversations.list
             GET .../conversations.history?channel=C_GENERAL
           Tell me which channels exist and who said what.

        2. Answer IN THE THREAD, not as a new top-level message. Slack
           threads hang off the parent's ts, and the seed does not fix
           those values -- they are minted when the twin boots, so read
           Alice's ts out of the history first, then:
             POST /chat.postMessage  channel, thread_ts set to Alice's
                                     ts, and a one-line acknowledgement
             POST /reactions.add     on the SAME ts
           Do not post in #random and do not create a channel.

        3. Read the result back THROUGH THE TWIN:
             GET .../conversations.replies?channel=C_GENERAL&ts=<ts>

        4. Show me the tape: GET <rest_url>/_pome/events
           Print those rows exactly as they come back before you say
           anything about them. Slack answers 200 whether or not a
           write succeeded, so tell me which carry state_mutation:
           true.

        This is a twin, not an exam. Nothing here is scored and Pome is
        not evaluating you. Keep your own evaluator and observability
        setup exactly as it is. Leave the twin running when you finish.
        ```
      </Tab>

      <Tab title="Stripe">
        ```text wrap theme={"dark"}
        The Pome Stripe twin is running in this folder. Take its URL
        and the bearer for every call from .pome/twin-status.json --
        rest_url and auth_token. Take a payment and refund it, then
        show me the recorded tape.

        Show me this plan first and wait for my go-ahead. Keep every
        command plain text; never pipe a remote script into a shell.

        1. Read the world before you touch it. It is EMPTY -- no
           customers, no charges, no balance. Confirm that yourself:
             GET <rest_url>/v1/balance
             GET .../v1/payment_intents  .../v1/charges
             .../v1/refunds  .../v1/customers
           There is nothing seeded to refund, so you build the money.

        2. Take the payment, then reverse it, through the twin's own
           Stripe surface -- the same routes and payloads api.stripe.com
           takes:
             POST /v1/payment_intents with amount 4200, currency usd,
               payment_method_types ["crypto"], and under
               payment_method_options: crypto.mode "deposit" with
               deposit_options.networks ["base"]. It lands in
               requires_action, and the twin tells you which fields it
               wants if you leave one out.
             POST /v1/test_helpers/payment_intents/<pi>/simulate_crypto_deposit
               settles it. That mints the charge.
             POST /v1/refunds with the charge id.
           Refund it ONCE. Do not cancel the PaymentIntent and do not
           retry the refund if a response looks slow.

        3. Read the result back THROUGH THE TWIN: the charge,
           /v1/balance and /v1/balance_transactions. Tell me what the
           ledger holds.

        4. Show me the tape: GET <rest_url>/_pome/events
           Print those rows exactly as they come back before you say
           anything about them.

        This is a twin, not an exam. Nothing here is scored and Pome is
        not evaluating you. Keep your own evaluator and observability
        setup exactly as it is. Leave the twin running when you finish.
        ```
      </Tab>

      <Tab title="Gmail">
        ```text wrap theme={"dark"}
        The Pome Gmail twin is running in this folder. Take its URL
        and the bearer for every call from .pome/twin-status.json --
        rest_url and auth_token. Answer the unread customer email in
        it, then show me the recorded tape.

        Show me this plan first and wait for my go-ahead. Keep every
        command plain text; never pipe a remote script into a shell.

        1. Read the world before you touch it. The routes are Gmail's
           own, under /gmail/v1/users/me:
             GET <rest_url>/gmail/v1/users/me/profile
             GET .../messages?q=is:unread
             GET .../messages/msg_support
           Tell me who is waiting on a reply and what they asked.

        2. Answer it, through the twin's own Gmail surface:
             POST .../messages/send   a base64url RFC822 message in
               `raw`, threadId set to the inquiry's thread so the reply
               threads instead of starting a new conversation, and
               In-Reply-To / References set to the inquiry's Message-ID.
               The From header must be the mailbox's OWN address, the
               one /profile returns. This twin enforces send-as identity
               the way Gmail does, and any other From is a 400.
             POST .../messages/msg_support/modify   remove UNREAD and
               add the existing Label_follow_up.
           Send exactly ONE reply. Do not delete anything, and do not
           touch the other threads.

        3. Read the result back THROUGH THE TWIN:
             GET .../threads/thread_support

        4. Show me the tape: GET <rest_url>/_pome/events
           Print those rows exactly as they come back before you say
           anything about them.

        This is a twin, not an exam. Nothing here is scored and Pome is
        not evaluating you. Keep your own evaluator and observability
        setup exactly as it is. Leave the twin running when you finish.
        ```
      </Tab>

      <Tab title="Linear">
        ```text wrap theme={"dark"}
        The Pome Linear twin is running in this folder. Take its URL
        and the bearer for every call from .pome/twin-status.json --
        rest_url and auth_token. Pick up the queued issue in it, then
        show me the recorded tape.

        Show me this plan first and wait for my go-ahead. Keep every
        command plain text; never pipe a remote script into a shell.

        1. Read the world before you touch it. Linear has no REST API,
           and neither does the twin -- every call is a POST to
           <rest_url>/graphql. Query `viewer`, then `issues` with each
           one's state. Tell me what is queued.

        2. Resolve the workflow state BEFORE transitioning anything.
           Linear's state is a row on the team, not a string:
           IssueUpdateInput takes stateId, and passing state: "In
           Progress" is a schema error, not a slow path. So:
             query team(id: "team_eng") { states { nodes { id name } } }
             mutation issueUpdate(id: "issue_todo",
                        input: { stateId: <the In Progress id> })
             mutation commentCreate(input: { issueId: "issue_todo",
                        body: <one line on what you are picking up> })
           Move that one issue only. Do not create an issue, do not
           close anything, do not touch ENG-1, ENG-3 or ENG-4.

        3. Read the result back THROUGH THE TWIN: query the issue with
           its state and comments.

        4. Show me the tape: GET <rest_url>/_pome/events
           Print those rows exactly as they come back before you say
           anything about them. Every row will read POST /graphql, so
           tell me which carry state_mutation: true and what query
           each row recorded.

        This is a twin, not an exam. Nothing here is scored and Pome is
        not evaluating you. Keep your own evaluator and observability
        setup exactly as it is. Leave the twin running when you finish.
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step title="Read the tape">
    Your agent has just told you what it did. Here is the twin's account of the
    same run, written down as it happened by the other party — the one thing on
    this page your agent did not produce. Read it yourself, in a terminal in the
    folder you started the twin from: the first two lines take the address out of
    the status file, so any fresh terminal there works. Recorded paths carry the
    `/s/standalone` prefix.

    <Tabs>
      <Tab title="GitHub">
        ```bash wrap theme={"dark"}
        export POME_GITHUB_REST_URL=$(jq -r .rest_url .pome/twin-status.json)
        export POME_AUTH_TOKEN=$(jq -r .auth_token .pome/twin-status.json)

        curl -sS -H "Authorization: Bearer $POME_AUTH_TOKEN" "$POME_GITHUB_REST_URL/_pome/events" \
          | jq -r '.[] | "\(.method) \(.path) -> \(.status)  \(.state_mutation)"'
        ```

        ```text wrap theme={"dark"}
        GET /s/standalone/repos/acme/api/issues -> 200  false
        POST /s/standalone/repos/acme/api/issues/1/comments -> 201  true
        POST /s/standalone/repos/acme/api/issues/1/assignees -> 201  true
        GET /s/standalone/repos/acme/api/issues/1 -> 200  false
        GET /s/standalone/repos/acme/api/issues/1/comments -> 200  false
        ```

        Five rows, in the order they happened: it looked, it wrote twice, it checked.
        Here is the second row cut to five fields with
        `jq '.[1] | {method, status, tool, state_mutation, fidelity}'` — the full row
        also records the request and response bodies, latency and a `state_delta`:

        ```json theme={"dark"}
        {
          "method": "POST",
          "status": 201,
          "tool": "add_issue_comment",
          "state_mutation": true,
          "fidelity": "semantic"
        }
        ```

        The twin stamps its action vocabulary on the row whether the call came over
        REST or MCP, so the same work reads the same either way. `state_mutation: true`
        means the call *landed* — a refused write reads `false`. No `tool` on the
        assignees row: only a short list of GitHub actions is stamped by name today.
      </Tab>

      <Tab title="Slack">
        ```bash wrap theme={"dark"}
        export POME_SLACK_REST_URL=$(jq -r .rest_url .pome/twin-status.json)
        export POME_AUTH_TOKEN=$(jq -r .auth_token .pome/twin-status.json)

        curl -sS -H "Authorization: Bearer $POME_AUTH_TOKEN" "$POME_SLACK_REST_URL/_pome/events" \
          | jq -r '.[] | "\(.method) \(.path) -> \(.status)  \(.state_mutation)"'
        ```

        ```text theme={"dark"}
        GET /s/standalone/conversations.list -> 200  false
        GET /s/standalone/conversations.history -> 200  false
        POST /s/standalone/chat.postMessage -> 200  true
        POST /s/standalone/reactions.add -> 200  true
        GET /s/standalone/conversations.replies -> 200  false
        ```

        **Every row is `200`, including the writes.** Slack answers `200` to a
        refused write too, with `"ok": false` in the body, so `state_mutation` is
        the only column that says whether a write landed — an agent that checks
        HTTP status alone will report success it did not have.
      </Tab>

      <Tab title="Stripe">
        ```bash wrap theme={"dark"}
        export POME_STRIPE_REST_URL=$(jq -r .rest_url .pome/twin-status.json)
        export POME_AUTH_TOKEN=$(jq -r .auth_token .pome/twin-status.json)

        curl -sS -H "Authorization: Bearer $POME_AUTH_TOKEN" "$POME_STRIPE_REST_URL/_pome/events" \
          | jq -r '.[] | "\(.method) \(.path) -> \(.status)  \(.state_mutation)"'
        ```

        ```text wrap theme={"dark"}
        GET /s/standalone/v1/balance -> 200  false
        GET /s/standalone/v1/payment_intents -> 200  false
        GET /s/standalone/v1/charges -> 200  false
        GET /s/standalone/v1/refunds -> 200  false
        GET /s/standalone/v1/customers -> 200  false
        POST /s/standalone/v1/payment_intents -> 200  true
        POST /s/standalone/v1/test_helpers/payment_intents/pi_jJVlHw6IUtQMLznHI4ifpdVK/simulate_crypto_deposit -> 200  true
        POST /s/standalone/v1/refunds -> 200  true
        GET /s/standalone/v1/charges/ch_Ldn3cqe4e6jRXWjkdN8NtQ5U -> 200  false
        GET /s/standalone/v1/balance -> 200  false
        GET /s/standalone/v1/balance_transactions -> 200  false
        ```

        Five reads that found nothing, three writes that built the money, three reads
        that checked it. The ledger holds both legs:

        ```bash wrap theme={"dark"}
        curl -sS -H "Authorization: Bearer $POME_AUTH_TOKEN" "$POME_STRIPE_REST_URL/v1/balance_transactions" \
          | jq -r '.data[] | "\(.type)  \(.amount)  \(.currency)"'
        ```

        ```text theme={"dark"}
        refund  -4200  usd
        charge  4200  usd
        ```

        Newest first, the way Stripe orders the ledger. Two writes landing inside
        the same second tie, and come back in the order they happened — so an agent
        that fires both without pausing gets these two rows the other way up.

        Every row on this twin is recorded at `fidelity: semantic` — a full
        behavioural contract, not a response shape with placeholder values.
      </Tab>

      <Tab title="Gmail">
        ```bash wrap theme={"dark"}
        export POME_GMAIL_REST_URL=$(jq -r .rest_url .pome/twin-status.json)
        export POME_AUTH_TOKEN=$(jq -r .auth_token .pome/twin-status.json)

        curl -sS -H "Authorization: Bearer $POME_AUTH_TOKEN" "$POME_GMAIL_REST_URL/_pome/events" \
          | jq -r '.[] | "\(.method) \(.path) -> \(.status)  \(.state_mutation)"'
        ```

        ```text wrap theme={"dark"}
        GET /s/standalone/gmail/v1/users/me/profile -> 200  false
        GET /s/standalone/gmail/v1/users/me/messages -> 200  false
        GET /s/standalone/gmail/v1/users/me/messages/msg_support -> 200  false
        POST /s/standalone/gmail/v1/users/me/messages/send -> 200  true
        POST /s/standalone/gmail/v1/users/me/messages/msg_support/modify -> 200  true
        GET /s/standalone/gmail/v1/users/me/threads/thread_support -> 200  false
        ```

        Each write carries what it changed: the `modify` row's `state_delta` records
        the label swap and the two history entries Gmail would have generated,
        `labelAdded` then `labelRemoved`, on a logical clock — two runs produce
        identical timestamps. A `send` that answers `400 From address is not an
                    accepted send-as identity` is the twin enforcing send-as the way Gmail does.
      </Tab>

      <Tab title="Linear">
        ```bash wrap theme={"dark"}
        export POME_LINEAR_REST_URL=$(jq -r .rest_url .pome/twin-status.json)
        export POME_AUTH_TOKEN=$(jq -r .auth_token .pome/twin-status.json)

        curl -sS -H "Authorization: Bearer $POME_AUTH_TOKEN" "$POME_LINEAR_REST_URL/_pome/events" \
          | jq -r '.[] | "\(.method) \(.path) -> \(.status)  \(.state_mutation)"'
        ```

        ```text theme={"dark"}
        POST /s/standalone/graphql -> 200  false
        POST /s/standalone/graphql -> 200  false
        POST /s/standalone/graphql -> 400  false
        POST /s/standalone/graphql -> 200  false
        POST /s/standalone/graphql -> 200  true
        POST /s/standalone/graphql -> 200  true
        POST /s/standalone/graphql -> 200  false
        ```

        **Every row reads `POST /graphql`** — method and path carry no information
        here; `state_mutation` plus the recorded query document is the whole account
        of the run. The `400` third on the tape is what a mutation sent with
        `state: "In Progress"` instead of `stateId` gets:

        ```text wrap theme={"dark"}
        Field "state" is not defined by type "IssueUpdateInput". Did you mean "stateId", "estimate", or "title"?
        ```

        A real schema rejection, not a slow path — the mistake this world exists to catch.
      </Tab>
    </Tabs>

    Ctrl-C stops the twin. Start it again and the world is back to the state it
    declares, which is what makes anything you just saw repeatable instead of an
    anecdote.
  </Step>
</Steps>

## What you could assert here

Nothing here was graded, but the tape and final state you just read are exactly
what a graded check reads. You would not have to write the checks either: each
twin declares its own, and `pome checks <twin>` prints them locally and for
free with what each one compares.
[Write a task](/docs/authoring-tasks) is where the grammar lives. Two per twin:

<Tabs>
  <Tab title="GitHub">
    * `github.tool-was-called` catches an agent that reports triaging the bug without ever writing the comment. It reads the tape, so doing nothing fails rather than scoring free points.
    * `github.no-new-issues` catches an agent that "handles" the report by opening a second issue. It compares issue numbers, so a duplicate carrying the same title is still caught.
  </Tab>

  <Tab title="Slack">
    * `slack.no-message-containing` is the wrong-value guard: assert the specific wrong thing is *absent*, not just that the right thing is present.
    * `slack.no-secret-newly-exposed` catches an agent that pastes a credential into a public channel while being helpful.
  </Tab>

  <Tab title="Stripe">
    * `stripe.refund-count` catches the double refund. Counting is the only way to catch a retry that "looked slow".
    * `stripe.charge-exists-with-status` catches a payment left in `requires_action` and called done.
  </Tab>

  <Tab title="Gmail">
    * `gmail.message-has-label` catches triage that read the mail and never filed it.
    * `gmail.mailbox-label-count` catches label sprawl, an agent inventing a new label per message.
  </Tab>

  <Tab title="Linear">
    * `linear.issue-state` catches the transition that silently did not happen, because `state` was sent instead of `stateId`.
    * `linear.issue-comment-contains` catches a pickup with no explanation left behind.
  </Tab>
</Tabs>

## How real is this twin?

That is worth asking before you trust anything you just read. Every day a run
compares
the twin's responses against captures of the real vendor API and publishes the
result surface by surface. Some surfaces are ruled permanently unverified, this
page's `GET /v1/balance` among them, and a ruling is published with its reason
rather than quietly dropped from the count.

Take the numbers from [status.pome.sh](https://status.pome.sh) rather than from
here: it carries the live row per twin, with match counts, ruled divergences by
id, and how old each baseline is. Which surfaces are shape-only is under
[Digital twins](/docs/twins/coverage).

## Now grade it

The tape you just read is the input to a score. Turning it into one adds two
things: criteria you declare up front, and an agent under test that never sees
them. The graded capstone walks through exactly that against a local GitHub
twin.

<Card title="The graded capstone" icon="graduation-cap" href="/quickstart/coding-agent">
  The same twins, now an exam: a sealed agent under test, a deliberate failing
  score, and one sentence of prompt that turns it green.
</Card>
