Handshake
Bilateral negotiation with escrowed settlement. Two parties trade structured offers - currency, items, and free-form terms on both sides - until one accepts, and the accepted bundle then settles through a stage-and-dual-confirm protocol that moves every leg or none of them.
When to reach for it: player-to-player trade windows, freelance scope-and-fee negotiation into escrow, agent-to-agent commerce, B2B quote flows - anywhere a deal is one-to-one, multi-issue, and half-completing it would be worse than not doing it at all.
When not to: one-to-many price discovery belongs to Auction; agreements with three or more signatories, staked collateral, and ongoing obligations belong to Pact; a movement of funds with nothing to negotiate is a plain Treasury transfer.
Concepts
- A session is bilateral and domain-scoped - exactly two parties inside a
{domain}namespace: the initiator (whoever calls create, always anauth_user) and one counterparty, anauth_useror anagent. The two ids must differ; a self-deal is400 invalid_party. - Bundles are two-sided and multi-issue - every offer states what
initiator_givesand whatresponder_gives, and each side may carry currency legs, item legs, and terms. A term markedbindingis meant to be enforceable after settlement; Handshake stores it and does not police it. - Alternation is enforced - you cannot counter your own standing offer,
and each counter is a new round.
max_roundscaps the negotiation. - Offers expire, lazily - each offer carries
expires_atand flips toEXPIREDthe next time anyone reads or acts on the session. No background job retires offers. - Settlement is three-phase and holds no value - accepting moves nothing, it creates a settlement both sides must stage and then both confirm. Staged currency sits in Treasury holds and staged items in Item Engine reservations; the settlement record stores only their ids. See Settlement and escrow.
Open a session
The initiator is always the caller. An opening offer is optional; without one the session starts at round 0 with no active offer.
snug handshake sessions create --domain docs4-hs \
--counterparty docs4-hs-other --counterparty-label seller \
--offer-file ./opening_offer.json
snug handshake sessions create --domain docs4-hs \
--counterparty-kind agent --counterparty agt_docs4_broker \
--offer-ttl 120 --max-rounds 6 --settlement-timeout 900Over HTTP the opening offer is inline:
curl -X POST -H "Authorization: Bearer $SNUG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"counterparty_kind":"auth_user","counterparty_id":"docs4-hs-other",
"opening_offer":{
"initiator_gives":{"currency_legs":[{"currency_id":"cur_docs4_gold","amount":250}]},
"responder_gives":{"item_legs":[{"item_id":"item_XfSfxrysezyA","quantity":1}]}}}' \
http://localhost:4000/api/v1/handshake/docs4-hs/sessions{
"status": 201,
"msg": "Created",
"data": {
"session": {
"session_id": "MafxtfutDwwrKQYzAjcX",
"domain": "docs4-hs",
"initiator": { "kind": "auth_user", "id": "docs-wave", "label": "initiator" },
"responder": { "kind": "auth_user", "id": "docs4-hs-other", "label": "responder" },
"status": "NEGOTIATING", "round": 1,
"active_offer_id": "cUuKRVLuHXAPUaXqUtLy", "settlement_id": null,
"policy": { "max_rounds": 20, "offer_ttl_seconds": 300,
"session_ttl_seconds": 86400, "settlement_timeout_seconds": 600 },
"created_at": "2026-08-28T23:02:19.375730Z"
},
"opening_offer": {
"offer_id": "cUuKRVLuHXAPUaXqUtLy", "round": 1, "status": "OPEN",
"proposed_by": "docs-wave", "in_reply_to": null,
"expires_at": "2026-08-28T23:07:19.377855Z", "bundle": { "...": "..." }
}
}
}Session and offer ids are opaque strings; the deal_... and off_... forms
in the CLI's own examples are illustrative, not a real prefix scheme. Only the
two parties can read a session - anyone else gets 403 not_a_party, which a
platform admin token bypasses.
Counter, accept, reject, withdraw
snug handshake offers counter --domain docs4-hs --id $SESSION --file ./round2.json
snug handshake offers accept --domain docs4-hs --id $SESSION --offer-id $OFFER
snug handshake offers reject --domain docs4-hs --id $SESSION --offer-id $OFFER
snug handshake offers withdraw --domain docs4-hs --id $SESSION --offer-id $OFFERThe rules the server enforces:
- Countering your own open offer is
409 cannot_counter_own_offer. A counter from the other side marks the standing offerCOUNTEREDand links the new one throughin_reply_to(as separate writes, which is why amax_roundsrefusal can leave the standing offer already countered; see the gotcha below). - Accepting or rejecting your own offer is
403 wrong_party, as is withdrawing someone else's - you may only withdraw your own. - Acting on an offer that is no longer
OPENis409 offer_not_open; accepting an expired one is409 offer_expired(reject and withdraw reportoffer_not_openfor it). - A counter past
max_roundsis409 max_rounds_reached.
Accepting starts settlement and returns the settlement record rather than the offer - that phase has its own page, Settlement and escrow.
Read the negotiation back
snug handshake history --domain docs4-hs --id $SESSION # transcript
snug handshake history --domain docs4-hs --id $SESSION --view concessions # per-round deltasThe default transcript view returns the offers in round order with their
status and reply chain, a page at a time (a
list filtering on round, proposed_by,
status, and created_at). concessions adds a per-round, per-party diff
over the whole transcript against the previous round - useful for an agent deciding whether the other side is
still moving. From a session where the initiator raised their
currency leg from 500 to 650 and tightened a term:
[{ "round": 2, "deltas": [
{ "party_id": "docs-wave",
"currency_deltas": [{ "currency_id": "cur_docs4_gold", "delta": 150.0 }],
"items_added": [], "items_removed": [], "terms_changed": ["delivery_window"] },
{ "party_id": "docs4-hs-other",
"currency_deltas": [], "items_added": [], "items_removed": [], "terms_changed": [] }
]}]Find sessions
snug handshake sessions list --domain docs4-hs --mine
snug handshake sessions list --domain docs4-hs --filter 'status[eq]=SETTLED'
snug handshake sessions get --domain docs4-hs --id $SESSIONA non-admin caller is restricted to sessions they are a party to whether or
not --mine is passed; the flag only changes anything for a platform admin,
who otherwise sees every session in the domain. The party scope is part of
the query, so pages are full and total=true counts only the caller's
sessions. The list speaks the
standard list contract: filters on
status, initiator_id, responder_id, created_at, and updated_at, and
sorts on created_at (newest first by default) and updated_at. status
takes the SCREAMING_SNAKE_CASE names; an unknown one is a 400.
Behaviors and gotchas
- A refused counter still kills the standing offer. A counter that comes
back
409 max_rounds_reachedhas already flipped the offer it was countering toCOUNTERED, leaving the session unresolvable: that offer can no longer be accepted and no new one fits in the round limit. Cancel is the only exit, so leave headroom inmax_rounds. - Expiry breaks the reply chain. A lazily expired offer clears
active_offer_id, so alternation lifts - the same party may immediately re-offer - and the replacement carriesin_reply_to: nulleven though it continues the same negotiation. - The
{domain}path segment is ignored on session-scoped routes - a session read under a domain it does not belong to is still returned. Only list and sweep filter on it. session_ttl_secondsis accepted and echoed but never enforced - a session created with--session-ttl 1was stillNEGOTIATINGfour seconds later. Sessions end only through settlement, cancel, or settlement timeout. TheSessionStatusenum likewise carries anAGREEDvalue nothing sets.
Limits and configuration
Round budget, offer and session lifetimes, the settlement window, and how much
transcript is retained are HANDSHAKE_* environment variables - 20 rounds, a
300-second offer TTL, and a 600-second settlement window by default, each
overridable per session at create time.
Reference
- Handshake API - every endpoint, callable
- Settlement and escrow - staging, dual confirm, rollback
- Related: Treasury holds the currency escrow, Item Engine holds the item escrow, Auction for one-to-many price discovery, Pact for multi-party staked agreements
