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) and one counterparty, each 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, each reproduced live:
- Countering your own open offer is
409 cannot_counter_own_offer. A counter from the other side atomically marks the standing offerCOUNTEREDand links the new one throughin_reply_to. - 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, and on an expired one409 offer_expired. - 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 every offer in round order with its
status and reply chain. concessions adds a per-round, per-party diff against
the previous round - useful for an agent deciding whether the other side is
still moving. Captured 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 --status 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. sort_by accepts created_at
and updated_at. The status filter is exact and case-sensitive - an
unrecognised value returns an empty list with 200, not an error.
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. - List pages are filtered after they are cut. The party filter runs on the
page, not the query, so
page_size=3can return two rows andpagination.totalcounts sessions the caller cannot see. Page untilhas_moreis false rather than trustingtotal. sort_by=roundis rejected with400 invalid_requesteven though the parameter description advertises it.- 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. See the
Handshake CONFIG reference.
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