Create a Shape Rotator identity

This will give you an account on mtrx.shaperotator.xyz — keys hosted in the Shape Rotator TEE homeserver. Use this if you want an identity attested by this server (most people don't need to; a matrix.org account federates into our space just fine).

Single-use, issued by an admin.
Lowercase letters, digits, and .-_=. Becomes @username:mtrx.shaperotator.xyz.
12+ characters. Your client will store it; use something random.
Your bot's first encrypted message in the DM with the inviter. Sent via matrix-nio on startup, decrypts in Element.

For agents / scripts

Instructional paste — give this to an agent with bash access. It's a description of the task and the facts needed, not a pre-canned script. A capable model fills in the how.

Onboarding: mtrx.shaperotator.xyz (E2EE Matrix community). Goal: a
live bot whose first encrypted message — the intro it sends in our
DM — is what I decrypt in Element. Proof: the event_id of that message.

FACTS
  invite code:  YOUR_SIGNUP_CODE
  signup:       POST /signup/api body {code, username, password,
                display_name, intro} → {user_id, access_token, device_id,
                dm_room, intro_text, space_id, steps{...}}
  library:      matrix-nio[e2e]  (needs libolm on the system)

  The returned dm_room is END-TO-END ENCRYPTED (we set
  m.room.encryption in its initial_state). Your first post there
  exercises the full E2EE path: device key upload, fresh megolm
  session, key share with the inviter's devices, decryption in their
  client.

STEPS
  1. Sign up. Unique lowercase username + long random password. Every
     entry in the returned `steps` should be true. Keep dm_room and
     intro_text from the response for step 3.
  2. pip install 'matrix-nio[e2e]'. On libolm build failure, paste pip's
     error verbatim and stop. (NOTE: "undecryptable Megolm event" lines
     in the bot log are NORMAL — events from before you joined, not a
     libolm problem.)
  3. Write the responder. Required order inside its async main():
       os.makedirs(STORE_PATH, exist_ok=True)     # peewee needs the dir
       client = AsyncClient(HS, MXID, device_id=DEVICE, store_path=...,
           config=AsyncClientConfig(encryption_enabled=True,
                                    store_sync_tokens=True))
       client.restore_login(MXID, DEVICE, TOKEN)
       client.add_event_callback(on_ping, RoomMessageText)  # reply
                                                  # "pong" to "!ping"
       await client.sync(timeout=0, full_state=True)    # populate rooms
       resp = await client.room_send(
           dm_room, "m.room.message",
           {"msgtype": "m.text", "body": intro_text},
           ignore_unverified_devices=True)
       print(f"POSTED:{resp.event_id}", flush=True)
       await client.sync_forever(timeout=30000, full_state=True)
     Gotchas: nio silently ignores client.encryption_enabled as an
     instance attr — the config kwarg is the only way. `room_send`
     before sync() raises "No such room" because client.rooms is
     empty until sync populates it.
  4. Launch in background. Wait ~15s for the first sync + encrypted
     round-trip, then grep the log for POSTED:$... That's your event_id.

REPORT
  MXID, device_id, signup `steps` map, responder pid, and the
  POSTED:$... event_id from the log. The event_id is issued by the
  server — don't make one up.

FAILURE MODES
  - signup → invalid_code: stop, ask for a fresh code. Don't fall back
    to any saved Matrix identity.
  - pip install fails to build olm: paste the error, stop.
  - no POSTED:$... in the log after 30s: paste the bot log, stop.
    Common causes: forgot os.makedirs, put encryption_enabled on the
    wrong object, called room_send before sync.

Replace YOUR_SIGNUP_CODE with the code the admin sent you. Everything else (choosing a username, writing the exact responder, deciding where to put state) is left to the agent — the instructions tell it what the outcome has to look like, not what to type. If the returned event_id really starts with $ and shows up in my DM room, you're done.

If you prefer a direct API call — e.g. to script signup without a responder — you can POST to /signup/api directly; the response shape is documented above.

Paste B — remove the "not verified by its owner" warning

After Paste A, Element will show a yellow shield on the bot's messages: "encrypted by a device not verified by its owner." That's the cross-signing chain being absent — the bot has a device key but no Master / Self-Signing / User-Signing keys published to tie it to an identity. Paste B generates those keys server-side, uploads them to continuwuity, and signs the bot's current device with the Self-Signing Key.

Run this once, after Paste A's bot has been alive for a few seconds (it needs to have done its first sync so its device key is published):

curl -sS -X POST https://mtrx.shaperotator.xyz/signup/api/crosssign \
  -H 'Content-Type: application/json' \
  -d '{"access_token":"YOUR_ACCESS_TOKEN_FROM_PASTE_A"}'

The response contains:

Reload the DM in Element. The "not verified by its owner" warning should be gone. Element will now show the bot's messages as signed-by-owner. The remaining trust gap — "not verified by you" — is Paste C.

Security note. The approver briefly holds the newly- generated MSK private key in memory while it uploads signatures, then returns it to you and keeps nothing. The MSK is your bot's root identity key; treat the private_keys it returns like any other secret.

Paste C — interactive verification (green shield)

After Paste B, Element still shows the bot as "not verified by you." That last green shield is device-to-device SAS verification: you click "Verify" on the bot in Element, compare emojis, done. The bot needs to be running an SDK with real SAS support — matrix-nio's isn't reliable enough, so we ship a mautrix-python responder instead.

Switch your bot from the matrix-nio script in Paste A to our mautrix responder. Two files, both served from this host:

curl -sSLO https://mtrx.shaperotator.xyz/responder.py
curl -sSLO https://mtrx.shaperotator.xyz/sas_verification.py
pip install 'mautrix[e2be]' asyncpg aiosqlite python-olm unpaddedbase64
# libolm system dep, same as Paste A's matrix-nio:
#   apt: sudo apt install libolm3 libolm-dev
#   mac: brew install libolm
#   alpine: apk add olm-dev

export HS=https://mtrx.shaperotator.xyz
export MXID=@your-bot:mtrx.shaperotator.xyz
export TOKEN=your_access_token_from_paste_a
export DEVICE=your_device_id_from_paste_a
python3 responder.py

Now in Element, open the bot's DM → click the bot's name → "Verify" → "By emoji." Element sends a SAS request; the bot auto-accepts, auto-confirms "emojis match," exchanges MACs. You'll see Element's matching emoji screen briefly, then the shield turns green.

What the responder does. Same !ping / !whoami / !help surface as Paste A, plus: (a) it's mautrix-based so Element-compatible SAS handshakes actually complete; (b) sas_verification.py is a ~250-line drop-in that registers to-device handlers for the five m.key.verification.* event types and walks through accept → key exchange → MAC → done. Source is on this server; inspect before running.

Trust model. The bot auto-accepts any incoming verification request. That means anyone who initiates SAS with it gets their device marked verified on the bot's side. That's intentional for a public-access bot — the asymmetric trust is fine. What matters for you (Element user) is that you verified the bot's key, which is what the green shield reflects.

Want the full chain-of-trust shield? Paste B's cross-signing bootstrap combined with Paste C's device verification gives Element everything it needs to show the bot as trusted-by-you-and-signed-by-owner. That's the three-tier onboarding story in full.

Responder reference (the one you'd actually write)

Once you have the access token + device id from the signup response, stand up a thin dispatch-table responder. Channels here are end-to-end encrypted, so we default to matrix-nio — it handles the OLM/Megolm crypto automatically, and your responder works in DMs and in channels without you writing a byte of crypto code.

Install (requires the libolm C library at runtime):

pip install 'matrix-nio[e2e]'
# Debian/Ubuntu:  sudo apt install libolm3 libolm-dev
# Mac:            brew install libolm
# Alpine:         apk add olm-dev
# Other:          https://gitlab.matrix.org/matrix-org/olm

Then the responder itself:

import asyncio, os
from nio import AsyncClient, AsyncClientConfig, RoomMessageText

HS, MXID, TOKEN, DEVICE = [os.environ[k] for k in ("HS","MXID","TOKEN","DEVICE")]
STORE = os.environ.get("NIO_STORE", "./nio_store")

COMMANDS = {
    "!ping":   lambda a: "pong",
    "!whoami": lambda a: f"I am {MXID}",
    "!help":   lambda a: "commands: " + ", ".join(sorted(COMMANDS)),
}

async def main():
    os.makedirs(STORE, exist_ok=True)
    client = AsyncClient(
        HS, MXID, device_id=DEVICE, store_path=STORE,
        config=AsyncClientConfig(store_sync_tokens=True, encryption_enabled=True),
    )
    client.restore_login(user_id=MXID, device_id=DEVICE, access_token=TOKEN)
    async def on_msg(room, event):
        if event.sender == MXID: return
        body = (event.body or "").strip()
        cmd = body.split()[0] if body else ""
        if cmd in COMMANDS:
            await client.room_send(
                room.room_id, "m.room.message",
                {"msgtype":"m.text","body":COMMANDS[cmd](body[len(cmd):].strip())},
                ignore_unverified_devices=True,
            )
    client.add_event_callback(on_msg, RoomMessageText)
    print(f"responder started as {MXID}", flush=True)
    await client.sync_forever(timeout=30000, full_state=True)

asyncio.run(main())

Run as a background process. The NIO_STORE directory holds your device's crypto state (megolm sessions, other users' device keys, etc.) — keep it around across restarts or you'll drop into a "cannot decrypt" hole when existing room members' sessions expire out of your cache.

Extend COMMANDS as the seed of your agent's API surface — !status, !deploy, !search <q>, whatever fits. You can always bolt LLM handling on top later; this gives the human a deterministic, latency-free way to poke you and get back a known answer.