SWARMHAUL REFERENCE LEG LIFECYCLE

Leg Lifecycle

From bid to settlement: how a Leg row and its on-chain PDA come into existence, travel through the protocol, and unlock payment.

The short version: the shipper never picks a courier. Agents bid, the coordinator's route optimizer picks the winning chain, legs are created atomically from the winning bids, and mirrored on-chain.

1. Shipper lists a package

  • Shipper's wallet signs list_package via the dashboard.
  • The instruction creates two on-chain PDAs: PackageAccount + VaultAccount (holds escrowed SOL up to maxBudgetLamports).
  • API persists the Package row with onChainPackage + onChainVault + listSignature. Status: listed.
  • WebSocket broadcasts PACKAGE_LISTED.

No legs yet. No swarm yet.

2. Agents bid

Every agent daemon polls GET /packages every 10s. For each package with status: listed:

  1. computeOptimalLeg — picks the segment of the agent's pre-declared itinerary that best overlaps the package's origin→dest. Returns pickup/dropoff coords, distance, estimated duration, and detour delta.
  2. detourExceedsLimit — hard gate on maxDetourKm / maxDetourMinutes from bidSettings.
  3. computeCost — fuel × distance + time × hourly rate, EUR → SOL. Canonical cost model; the API reuses it.
  4. reasonAboutBid — LLM call to gpt-oss-120b reasoning about whether to bid. Always has a rule-based fallback if the LLM is unavailable.
  5. If shouldBid, the agent POSTs to /bids with ed25519-signed headers (X-Pubkey, X-Nonce, X-Signature).

A bid is a proposed leg. It is not yet a leg — the coordinator still has to pick winners.

3. Coordinator evaluates — evaluateSwarmFormation

Triggered on every accepted bid. Threshold: MIN_BIDS_FOR_EVALUATION = 1 (as soon as the first bid lands, the coordinator checks if a swarm can form). Inside a serializable Postgres transaction:

  1. Re-fetch the package. Bail if it's not listed or if a swarm already exists (idempotency against races).
  2. Pull all non-expired bids for the package.
  3. Pull reputation rows for every unique bidder → repMap: Map<pubkey, score/100>.
  4. findOptimalRelayChain — picks either the single best bid or a multi-hop chain whose waypoints form a valid origin→dest relay, total cost ≤ maxBudgetSol, and whose reputation-weighted score is best. Bounded γ=0.08 nudge toward higher-rep chains prevents cartels.
  5. allocateReputationWeightedPayments — splits the payout pool across the winning bids: α=0.7 of each payment is proportional to the bid itself; 1−α is weighted by reputation. First-meeting Sybil ceiling at 0.6.
  6. tx.swarm.create with nested legs.create — one Leg row per bid in the chain.
  7. Package status flips to swarm_forming. Transaction commits.

Legs now exist in Postgres. Still no on-chain accounts.

4. On-chain commit — coordinatorFormAndAssignSwarm

After the Postgres transaction commits, the coordinator keypair executes one Solana transaction that bundles:

  1. form_swarm(totalLegs, totalLamports) — creates SwarmAccount PDA at [b"swarm", package]. Reserves the total payout from the vault.
  2. N × assign_leg(legIndex, courierPubkey, paymentLamports) — creates LegAccount PDAs at [b"leg", swarm, [legIndex]] for each leg. Each assign bumps the courier's on-chain reputation legs_accepted counter.

Post-confirm, the API updates swarm.onChainSwarm, derives and writes each leg.onChainLeg, and upserts AgentReputation rows in Postgres. WebSocket broadcasts SWARM_FORMED.

If on-chain fails, the Swarm row is marked failed and the package reverts to listed so a retry can form a fresh swarm.

5. Delivery + confirmation

Confirmation model (multi-leg aware). Legs confirm in strict index order. The program enforces leg_index == swarm.completed_legs, so leg i+1 cannot confirm until leg i has.

  • Final leg (leg_index == total_legs - 1): the shipper signs confirm_leg. next_leg_account must be None.
  • Intermediate leg (leg_index < total_legs - 1): the next-hop courier signs confirm_leg — their signature is the handoff attestation.
  • Errors: LegOutOfOrder, MissingNextLeg, UnexpectedNextLeg, UnauthorizedRecipient.

On-chain, confirm_leg transfers leg.agreedPaymentSol directly from the vault to the courier's wallet via a PDA-signed system transfer. Reputation legs_completed bumps. The off-chain mirror then applies the Bayesian ramp: new_score = old + (1 − old) × gain_factor × 0.05 (diminishing returns toward 1.0). The on-chain reliability_score stores the result scaled to 0–100.

State machine summary

Package:   listed → swarm_forming → in_transit → delivered
                                              ↘ failed (on-chain error, retry path)
Swarm:     forming → active → settled
                           ↘ failed
Leg:       pending → completed (in strict legIndex order)

Key invariants

  • A package has at most one active swarm (unique constraint + serializable tx).
  • A leg's agreedPaymentSol is locked at creation time — it does NOT recompute on settle.
  • The vault's lamport balance at confirm_leg time must cover leg.payment_lamports.
  • confirm_leg marks the leg complete before the system transfer (CEI pattern).
  • Legs confirm in strict legIndex order. Out-of-order confirms are rejected on-chain with LegOutOfOrder.

Why this architecture

  • Shipper doesn't pick couriers. The market picks; the coordinator executes allocation. Shippers can't prefer bad actors who offer side payments.
  • Legs are created from bids, not quotes. Every winning leg has a committed, signed bid behind it — no phantom couriers.
  • Single serializable tx creates the whole swarm. Prevents partial swarms under concurrent bid arrivals.
  • On-chain reputation lives in a PDA; Postgres mirrors it. Fast dashboard reads, durable on-chain source of truth. Reputation changes only via protocol actions (assign_leg, confirm_leg) — no standalone update_reputation instruction exists.
  • Shipper signs confirm_leg. Only the recipient can honestly attest "I received the goods." Courier self-attestation is a trust hole.