This is a hands-on tutorial. To keep it concrete, we follow two developers —
Tony (the host) and Peter (the buddy) — through
how their machines actually connect, and how the work moves once they're connected.
Pick a path below; each one takes a different route to the same win.
// SELECT YOUR LEARNING PATH
Choose your path
NOW PLAYING · The Architect — end-to-end
↕ switch path
Chapter 1
The broker lives on Tony's machine
When Tony starts Sys-Buddy, the broker (the little server that coordinates everything) runs on his own computer at localhost:8787. "localhost" just means "this very machine."(8787 is just the default MCP port — the broker can run on any port Tony picks; the examples below use 8787.)
Here's the whole problem in one picture — localhost is useless to Peter, because on Peter's laptop, "localhost" means Peter's laptop:
TONY'S LAPTOPPETER'S LAPTOP
broker → localhost:8787 Peter types localhost:8787
✅ "that's me, here it is" ❌ reaches HIS OWN laptop,
not Tony's. Nothing there.
So Peter needs some address that leads to Tony's machine specifically. Everything else here is just "how does Peter get that address, and how does his request travel there."
Chapter 2
Address vs token: where vs who
This is the single most important idea, and it unlocks the rest. Two totally different jobs, done by two different things:
The address = WHERE
Like the street address on an envelope. It only says where the request is going — to Tony's broker. It says nothing about who sent it.
The token = WHO
Like a signed ID inside the envelope. Every request carries Authorization: Bearer <token> — that's what proves "this is Peter, on the frontend role."
When Peter pairs, his connect command literally spells out both halves:
claude mcp add --transport http sys-buddy \
http://100.101.102.103:8787/mcp \ ← WHERE (Tony's broker)
--header "Authorization: Bearer PETER_TOKEN" ← WHO (it's Peter)
Tony and Peter dial the same broker address, but each request is stamped with a different token — so the broker always knows who's who. A leaked address alone is harmless: no token → 401, door stays shut.
Chapter 3
How Peter's request actually travels
Peter's Claude agent makes the network call — same as if Peter typed curl in a terminal. Here's the key idea: Claude doesn't do the networking itself. It hands the request to the operating system and says "please deliver this." The OS decides the route.
So the real question is: does Peter's OS know a route to Tony's machine? For an ordinary public web address, yes — every machine already knows how to reach the public internet. For a private address, it only works if something has taught the OS that route:
Peter's Claude: "OS, connect me to Tony's broker"
│
▼
Peter's OS: "do I have a route to that address?"
│
┌────┴────────────────────────────────┐
│ public address (ngrok) → │
│ "sure — it's on the internet, │
│ I already know the way." ✅ │
├─────────────────────────────────────┤
│ private address + Tailscale on → │
│ "that's Tony's machine on our │
│ tailnet — routing it there." ✅ │
├─────────────────────────────────────┤
│ private address, no Tailscale → │
│ "no route. request dies here." ❌ │
└─────────────────────────────────────┘
The token proves who Peter is, but without a route his machine can't reach Tony at all — so he never even gets to show the token. Supplying that route is the whole job of the two paths: ngrok gives Tony a public address (nothing for Peter to install), while Tailscale builds a private network both machines join. That's the fork — pick either path from the roster to see it in full.
Tiny precision:100.101.102.103 is a raw number, so there's no DNS name-lookup for it — DNS only turns names like google.com into numbers. Either way it still goes through the OS's routing step — which is exactly where a private-network tool like Tailscale hooks in.
Chapter 4
Three addresses, one broker
Same broker every time; only the address out front changes depending on the road:
Same machinebuddy = host
http://localhost:8787/mcp
ngrokpublic tunnel
https://abc-123.ngrok-free.app/mcp
Tailscaleprivate network
https://your-machine.tailnet.ts.net/mcp
ngrok makes a public address — every machine already knows how to reach public addresses, so Peter installs nothing. Tailscale makes a private address that only works between machines running Tailscale. That's the whole trade-off.
Chapter 5
Two doors: act, and watch
The broker answers at two paths. Once Peter's machine can reach the address, any program on it — Claude and his web browser — can use both:
Peter's Claude → http://100.101.102.103:8787/mcp← the tool door (act)
Peter's browser → http://100.101.102.103:8787/ui?v=…← the dashboard (watch)└──────── same broker, same address ────────┘
Peter opens the dashboard in his browser at the same address. The ?v= viewer token proves he's allowed to watch (read-only). Address gets the browser there; token says what he may do.
Bonus
Is the invite link also the dashboard?
No — but redeeming it produces one. When Peter redeems the sb1_ invite, pairing hands back three things:
sb1_a91f…inviteone-time · burned on redeem
→redeem
The claude mcp add … command
run in the terminal — wires Claude to the broker
The role prompt
paste into the Claude session
A personal dashboard link read-only<broker>/ui?v=<viewer-token>
sb1_… · the invite link
A one-time pass to pair. Used once, then burned. It can't do anything afterward.
…/ui?v=… · the dashboard link
A separate, reusable, read-only link with its own viewer token. Open it anytime to watch — but you can't act through it.
What each person ends up holding
Two kinds of credential: one to act (scoped, powerful) and one to watch (read-only).
Tony (host)
ACT
Own agent seat
An agent token scoped to {this task, the host's role}. Tony drives a role too — this is his seat at the table.
HAND
An invite link per other role
One sb1_ link for each role Tony isn't playing. Handed out to buddies, burned on redeem.
WATCH
Own dashboard link
A read-only …/ui?v=… link to watch the whole task progress live.
Peter (buddy)
ACT
Agent token
Scoped to {this task, Peter's role}. Can't touch other tasks or impersonate another role.
WATCH
Dashboard link
The read-only …/ui?v=… link produced at pairing. Watch anytime; act never.
Now pick a road →
NOW PLAYING · ngrok — the public tunnel
↕ switch path
The Quick Draw
ngrok, start to finish
The easy sibling — only Tony installs anything. ngrok is a small program Tony runs that creates a public web address and quietly forwards everything to his own localhost:8787. Peter needs nothing but the link.
1
Tony · starts ngrok in a terminal
Separate from the app. This says: give me a public https URL and forward anything that hits it to my local port 8787.
Into the Public URL field (leaving "Private network" OFF — ngrok is public, not a VPN), then clicks Create & start broker. Now the chain is live: public URL → ngrok → localhost:8787.
3
Tony · shares the invite link
The sb1_ link now embeds the ngrok URL as the broker address — an address that actually works from Peter's machine, unlike localhost.
4
Peter · joins — installs nothing
He pastes the link into Join and runs the command it gives him. His OS already knows how to reach public web addresses, so there's no "road" to set up.
Peter's Claude → ngrok's servers (over https) → Tony's ngrok program → Tony's broker. The public leg is TLS-encrypted, which is exactly why https is required — the token rides that leg and must never travel in cleartext.
Peter's OS already knows how to reach a public ngrok-free.app address — that's why he installs nothing. The whole "road" is the public internet.
Tony's worry
"A public URL now points at my machine — didn't I just expose myself to the whole internet?"
The reality
A URL is not access. The address is only where; a valid token is who. Anyone who finds the URL and knocks without a token gets a flat 401.
On top of that: https is enforced on the public leg, invites are single-use and expire in 15 min, tokens are scoped and auto-expire, pairing is rate-limited, everything is audit-logged, and closing the task revokes it all at once. See the full security list →
Try another →
NOW PLAYING · Tailscale — the private network
↕ switch path
The Ghost
Why do both people need Tailscale?
Because Tailscale isn't a link you share — it's a private network you join. That one fact is the whole difference from ngrok. Two ways to let someone reach your house:
ngrok = a public street address+ a locked door
A public address anyone can walk up to. Peter needs nothing installed — just the address (link) and the key (token).
one-sided · only the host sets it up
Tailscale = a gated communityoutsiders can't reach it at all
The broker lives inside a private network outsiders can't reach. To walk up to the door, Peter must become a resident first — install Tailscale and join. You can't mail someone the inside of a private network.
two-sided · both install & join
So the 100.x.y.z address in the link only means something to a machine that's also on the tailnet. That's why both need it.
Step by step
Tailscale, start to finish
1
Both · install Tailscale & log in
The step ngrok never had. Both machines join the same private network (a "tailnet"), each reachable by the other but invisible to the public internet.
2
Tony · runs tailscale serve
This publishes the broker on the tailnet at an https …ts.net address with a real TLS certificate — no raw IPs, no plain http.
Every call rides the WireGuard-encrypted tailnet straight to the broker. No public internet, no third-party servers in the middle — often a direct peer-to-peer link.
The reward for the extra install: no public URL, no third-party servers in the middle — often a direct, WireGuard-encrypted peer-to-peer link between the two machines. Maximum privacy.
Try another →
NOW PLAYING · The Flow — one todo, idea to verified
↕ switch path
Chapter 1
A todo carries two state fields
Once Tony and Peter are connected, the work itself gets broken into todos — one deliverable each. And this is where nearly everyone trips: a todo has two separate state fields, and the dashboard shows both at once. They are not two views of the same thing.
status = the agreement
How much have we agreed? It moves as people say yes — first to the idea, then to the shape of the interface. Nothing about it says anything has been built.
state = the march
How far has it been built? It moves as work actually lands — a contract on the table, a signed contract, a live endpoint, checks run, done.
status ›pending→accepted→contracted→verified/droppedhow much have we agreed?
state ›open→contract_proposed→contract_locked→backend_live→testing→verifiedhow far has it been built?
The dashboard doesn't make you hold six state names in your head — its mini-stepper collapses the march into four words. Note that backend_live and testing both light the same lamp:
mini-stepper: planning building readyverified────────────────────────────────────raw state: open contract_locked backend_liveverified
contract_proposed testing
Why two fields at all: agreeing and building are genuinely different clocks. A todo can be fully agreed (contracted) and still have zero lines of code behind it. Reading only one of the two is how "I thought that was done" happens.
Chapter 2
One todo, start to finish
Every step below is somebody typing a short command to their own agent — not to each other. The agent calls the broker; the broker decides whether the move is allowed. Tony drives the backend role, Peter the frontend role.
1
Agree WHAT · Tony proposes the deliverable
Proposing is the proposer's own acceptance — Tony is already counted in. status starts at pending. Every other named party accepts with yes #1, or bounces it back with no #1 <why>. A bounce isn't a dead end: Tony answers it with a new version, which resets everyone's acceptance so the reason actually gets read. When all named parties have accepted → status = accepted.
todo Login endpoint ← Tony proposes (counts as his own yes)
yes #1 ← Peter accepts
no #1 needs a refresh token ← …or bounces it back, with the reason
2
Agree HOW · one party proposes the contract
The contract is the exact shape of what's being handed over. Here it's an API, so that means route, request, response, errors — but a contract can just as well name screens, data shapes, or plain criteria, because not every handover between two agents is an HTTP endpoint. state = contract_proposed. And here's the part worth slowing down for: whoever proposes the contract becomes the producer for that todo.
pc #1 ← whoever types this is now the producer for todo #1
3
Sign · every party signs
When the last signature lands → state = contract_locked, status = contracted. The staging_url is withheld until every party has signed — you cannot get the thing you want (somewhere to point your code at) without first reading the shape you're agreeing to. That's the incentive, built into the order.
sign #1 ← each party signs
staging_url is released only on the LAST signature
4
Build · the producer — and only the producer — reports ready
When their side is actually live to build against, they say so. state = backend_live. Nobody else can say it on their behalf.
ready #1 ← producer only
5
Check · the consumers report back
Every party except the producer is a consumer for this todo, and they're the ones who test it. state = testing. The producer may not check its own work — that's the whole point of having a second party. A block is a strike; three strikes and the broker pulls the cord: that deliverable is flagged stuck, humans own it, and the other deliverables keep marching.
ok #1 ← consumer: it works
block #1 ← consumer: it doesn't (strike 1 of 3)
6
Verified · the deliverable is done
state = verified, status = verified. Both clocks finally read the same word. The task concludes when the last todo is verified — not the first.
done #1
Back to step 2, because it's the one people get wrong: producer is derived per todo, from whoever proposed the contract that got locked. It is not hardcoded to "backend." On the same task, Tony's backend can produce todo #1 while Peter's frontend produces todo #2 — each one the consumer of the other's work:
todo #1 contract proposed by Tony (backend) → Tony produces, Peter checks
todo #2 contract proposed by Peter (frontend) → Peter produces, Tony checks
Chapter 3
The broker enforces the turn order
None of the above is a convention that everybody politely follows. Each move is checked before it lands, and refused if it's out of turn:
pc #Npropose contract
needs status = accepted
sign #Nsign contract
needs a live proposal
ready #Nreport live
producer only · contract_locked
ok #N · block #Nreport a check
consumers only · backend_live
done #Nmark verified
needs state = testing
all of themevery move
named parties only
Read top to bottom, that table is just the turn order restated as refusals: agree WHAT before HOW; you can't sign air; you can't call yourself live before the shape is locked; you can't grade your own homework; and you can't call it done before anyone checked it.
Two sharp edges worth naming. First, ready #N is also refused while a newer contract version is in flight awaiting signatures — even if an older version is already locked. You can't declare yourself live against a shape people are still being asked to re-sign.
Second, a seat on the task is not a seat on every todo. Someone the todo doesn't bind can read it perfectly well on the dashboard — but they can never block it and never sign it. Being in the room isn't the same as being on the hook.
Chapter 4
Why #N is not optional
Before todos existed, "backend is ready" meant something. The moment a task carries six deliverables, it means nothing at all — ready on which one? So ready, ok, block and done all require the #N. The ambiguity isn't discouraged, it's removed by construction:
"backend is ready"→ ready on which of the six? refused — needs #Nready #4→ exactly one deliverable. unambiguous
The one exception:stuck may be used without a number — and that's deliberate, because without a #N it escalates the whole collaboration, not one deliverable. If you only meant one, say stuck #4.
Chapter 5
The two traps
Almost every "the broker is broken" message turns out to be one of these two.
Trap 1
"I typed sign #1 and nothing happened. Did it not go through?"
Signing needs something to sign
A signature attaches to a proposal. If nothing has been proposed on that todo, sign #1 has nothing to attach to — so it does nothing at all.
Nine times out of ten the answer is that nobody has typed pc #1 yet. Agree WHAT, propose HOW, then sign.
Trap 2
"It's said contract_locked for an hour. Something must be stuck."
Nothing auto-advances
Every arrow in that chain is a person deciding. A locked contract does not start a build. contract_locked sitting there forever isn't a bug — it means the producer hasn't typed ready #N yet.
The broker never moves a todo on its own. It only ever refuses moves that break the rules. So when something looks stuck, the question is never "what's wrong with the broker" — it's whose move is it, and have they typed it?
And you don't have to work that out yourself. The dashboard prints it straight onto the todo card — who owes the move, and the literal shorthand they need to type:
#1 Login endpoint contract_locked · contracted
planning ──building── ready ── verifiedNext: Tony (backend) — type ready #1
Whose move it is stops being folklore. It's on the card.
-Free tier hands out a new random URL each restart
-Traffic transits a third party (ngrok's servers)
VS
TAILSCALE
Private Network · The Ghost
+Nothing is ever exposed to the public internet
+Direct, WireGuard-encrypted peer-to-peer link
+Stable private IP that doesn't change with networks
+Perfect if you're already on a company tailnet
-Both people must install Tailscale & log in
-Two invites to manage: tailnet + sys-buddy
-More moving parts to explain to a new buddy
-Overkill for a quick one-off pairing
The verdict
Pick ngrok to pair in about 60 seconds with zero setup on the buddy's side — the tokens keep the public URL safe. Pick Tailscale when privacy is paramount, nothing may touch the public internet, or you're both already on a company tailnet. Same broker, same result either way.
Both are free to start. Tailscale's Personal plan is free for non-commercial use (up to 6 users, unlimited devices), and ngrok's free tier works fine too — it just hands you a fresh random URL on each restart.
Read one in full →
You reached the end
// FINISH HIM — er, the setup
Flawless Learning
You now know how two machines find each other, prove who they are, watch it happen — and how one deliverable travels from idea to verified. Go ship something with a buddy.