Skip to main content

Machine-to-machine handshake

Two parties want a session. Neither wants to take the other's word. A custom contract gives them a four-step handshake — proposed → accepted → keyed → active — with each step's hash committed on both state machines' chains.

This is the framework's answer to "we want a stateful relationship between machines without a third-party broker."


1. Both parties spin up

# Party A
curl -sS https://zeqapi.com/api/chain/state-machines \
-H "Authorization: Bearer ${A_ZSM_KEY}" \
-H "Content-Type: application/json" \
-d '{"slug":"alice","is_public":true}'

# Party B
curl -sS https://zeqapi.com/api/chain/state-machines \
-H "Authorization: Bearer ${B_ZSM_KEY}" \
-H "Content-Type: application/json" \
-d '{"slug":"bob","is_public":true}'

is_public: true so each side can read the other's chain without the other's API key.

2. A deploys the handshake contract on its slug

The handshake isn't a catalogue template — it's a small custom contract you POST to your machine's create route. Four states, three transitions you drive by hand:

curl -sS https://zeqapi.com/api/chain/alice/contracts \
-H "Authorization: Bearer ${A_ZSM_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "m2m_handshake", "version": "1.0",
"states": { "proposed": {"initial": true}, "accepted_pending": {}, "keyed": {}, "active": {"terminal": true} },
"transitions": [
{ "from": "proposed", "to": "accepted_pending", "operator": "KO42", "proof_required": false, "auto": false },
{ "from": "accepted_pending", "to": "keyed", "operator": "KO42", "proof_required": false, "auto": false },
{ "from": "keyed", "to": "active", "operator": "KO42", "proof_required": false, "auto": false }
],
"observers": [], "audit_clock": true, "zeqond_tick_rate": 1
}'

Save the returned id as ${A_CONTRACT_ID}. B does the same on its machine, saves ${B_CONTRACT_ID}.

3. A proposes — proposed → accepted-pending

# A side
curl -sS https://zeqapi.com/api/chain/alice/contracts/${A_CONTRACT_ID}/transition \
-H "Authorization: Bearer ${A_ZSM_KEY}" \
-H "Content-Type: application/json" \
-d '{
"to": "accepted_pending",
"input": {
"peer_slug": "bob",
"peer_pubkey": "<B-public-static-key>",
"session_nonce": "<random-32-byte-hex>",
"self_pubkey": "<A-public-static-key>"
}
}'

A's chain now has a contract row stating "I propose a session with bob, here's my nonce and my pubkey." The hash of that proposal is the state_hash of the entangled state row.

B reads A's chain to find the proposal:

curl -sS "https://zeqapi.com/api/chain/alice/explore?from=$(($(date +%s) * 1000 / 777))&limit=20" \
-H "Authorization: Bearer ${B_ZSM_KEY}"

(is_public machine — no auth needed for B, but it's a habit to send your key anyway.)

4. B accepts — accepted_pending → keyed

# B side — drive B's contract
curl -sS https://zeqapi.com/api/chain/bob/contracts/${B_CONTRACT_ID}/transition \
-H "Authorization: Bearer ${B_ZSM_KEY}" \
-H "Content-Type: application/json" \
-d '{
"to": "keyed",
"input": {
"peer_slug": "alice",
"session_nonce": "<same-nonce-from-Astep>",
"ecdh_pub": "<B ephemeral ECDH pub>",
"ack_hash": "<sha256 of A proposal row>"
}
}'

B's chain now has a row stating "I accept alice's proposal <ack_hash>, my ephemeral ECDH pub is <…>." The ack_hash ties B's row to A's exact proposal.

A reads B's chain, sees the acceptance, drives its own accepted_pending → keyed:

curl -sS https://zeqapi.com/api/chain/alice/contracts/${A_CONTRACT_ID}/transition \
-H "Authorization: Bearer ${A_ZSM_KEY}" \
-H "Content-Type: application/json" \
-d '{
"to": "keyed",
"input": {
"peer_slug": "bob",
"ecdh_pub": "<A ephemeral ECDH pub>",
"ack_hash": "<sha256 of B accepted row>"
}
}'

Both chains now hold the symmetric ECDH public exchange + cross-acks.

5. Active — keyed → active

Once both sides see the other's keyed row, both drive keyed → active:

# Both sides do this independently:
curl -sS https://zeqapi.com/api/chain/${MY_SLUG}/contracts/${MY_CONTRACT_ID}/transition \
-H "Authorization: Bearer ${MY_ZSM_KEY}" \
-H "Content-Type: application/json" \
-d '{
"to": "active",
"input": {
"peer_keyed_hash": "<sha256 of peer keyed row>",
"session_id": "<derived from nonce + both ECDH pubs>"
}
}'

The session is now live. Out-of-band, both sides derive the symmetric session key from the ECDH exchange. The framework never sees the symmetric key.

6. Use the session

Off-chain. Both parties hold the symmetric key. Both parties' chains witness the handshake and the session id. To prove the session existed, the auditor walks both chains and checks the cross-acks tie up.

If either side's chain breaks (pohc/validate returns valid: false), the session is suspect — but the rest of both chains is still valid. The handshake doesn't depend on continuous chain liveness; it depends on the rows that exist being immutable.


What this gives you that a TLS handshake doesn't

  • Both parties' acks are on the entangled state. A TLS handshake leaves no on-chain record. M2M does. If the session is later disputed, you have proof of who agreed to what at which Zeqond.
  • Symmetric, neither side trusted. Each party drives their own contract on their own state machine. Neither side can unilaterally change the other's record.
  • Tally tokens for receipts. Each successful transition mints a tally token. Both parties hold tokens stating "this handshake step happened with this peer at this Zeqond."
  • Compatible with off-chain transport. The framework witnesses the handshake; the actual session traffic flows over your favorite transport (WebRTC, libp2p, raw TCP, anything).

When NOT to use this

  • If you need fast (sub-Zeqond) handshakes for a real-time game, the entangled state commit adds latency. Use TLS or QUIC and beacon to your machine for retroactive auditability.
  • If you don't care about audit, a normal Diffie-Hellman + signature is enough. M2M handshake is overkill if no one will ever ask "did this session exist?"

For the typical case — two services that want a stateful, audited, peer-to-peer relationship — a custom handshake contract is the substrate.