# Plantaré — the bilateral signed record

Status: **v1 shipped (2026-07-14).** The two-sided signed Plantaré — the "formulario
bilateral firmado" other docs waited on — is now built end-to-end: transport in
`commons_core`, schema v12 + repo, `PlantareService`, and UI (propose from a chat,
accept/decline on the ledger). What's left is noted under "Deferred to a follow-up"
below. This doc keeps the original design for reference; the "Open questions" are now
resolved inline.

Built (commit series `feat(plantare): …`, 2026-07-14):
- **Transport** — `PlantarePledge` (canonical, byte-stable core + two BIP-340 Schnorr
  stubs), `PlantareCrypto` (sign/verify over SHA-256 of the core), `PlantareTransport` +
  `NostrPlantareTransport` (propose→accept→counter-sign / decline over NIP-17 gift wrap
  carrying a tagged JSON payload; the message inbox skips these and vice-versa).
- **Schema v12** — the deferred columns on `Plantares` (`pledgeId`, `debtorKey`,
  `creditorKey`, `debtorSignature`, `creditorSignature`, `movementId`, `remoteState`,
  `returnKind`, `workHours`), all nullable/defaulted so v1 local rows are untouched;
  guarded migration + schema dump + migration test.
- **`PlantareService`** — signs my stub, sends, and reconciles incoming moves into the
  ledger (verifies stubs before storing; pending proposals held in memory keyed by
  pledge id, relay redelivers on reconnect).
- **UI** — propose sheet reached from the chat overflow menu; accept/decline + handshake
  badges (awaiting signature / signed by both / declined) on the Plantares screen.

This doc originally specified the two-sided Plantaré so a later session could build it
without re-deciding. It is the "formulario bilateral firmado" that other docs wait on:

- [`data-model.md` §2.7](data-model.md) — the `Plantare` entity, whose `debtor_key`,
  `creditor_key`, `debtor_signature`/`creditor_signature` and `movement_id` columns are
  specified there but **deferred** in the shipped Drift table.
- [`sharing-model.md` §1, §6](sharing-model.md) — the Plantaré as the *private bilateral
  close* of an exchange; and the open question of anchoring reputation to a closed deal.
- [`PLAN.md` §5-bis](../../PLAN.md) — the paper original (BAH-Semillero, 2009): "partida
  doble repartida entre dos bolsillos", device-to-device, provenance DAG.
- [`open-decisions.md`](open-decisions.md) — strong rating anchoring "espera al formulario
  bilateral firmado".

## What ships today (v1, local-only)

A private local ledger only. `apps/app_seeds` `Plantares` table stores `varietyId`,
`direction` (`iReturn` / `owedToMe`), `counterparty` (**free text**), `owedDescription`,
`madeOn`, `dueBy`, `status`, `note`. No identity keys, no signatures, no transport — the
other party gets **no record**. The UI now surfaces the linked variety and the dates
(Phase 1), but it is still one-sided.

## The paper original (v0.4, transcribed)

Source: BAH-Semillero, *Plantaré — moneda comunitaria de intercambio de semilla*, v4.0,
© 2009–2010, CC-BY-SA 3.0 (`plantare.ourproject.org`). The printed form is **two matching
stubs** ("partida doble") on one sheet:

**Stub A — "CORTAR Y ENTREGAR A CAMBIO DE LAS SEMILLAS"** (cut off and handed over with the
seeds; the **giver keeps it** as proof of what they're owed):
- **Yo** ______ (your name)
- **localizable en** ______ (*email, tel., etc.* — contact info)
- **recibo** ______ (*cantidad de semillas o lo que sea que recibo*)
- **y me comprometo a devolver antes de (mes/año):** __ / 20__
- return options (*choose those that apply*):
  - `[ ]` una **cantidad similar\*** de la recibida  — *(\*) similar = no hibridada, no
    transgénica, cultivada en ecológico*
  - `[ ]` ó un **trabajo de ___ horas** equivalente
  - `[ ]` ó ______ (free text — anything else)
- **Firmado,** ______ **el** __ / __ / 20__ (signature + date)

**Stub B — "RESGUARDO QUE ME QUEDO"** (the receipt the **receiver keeps** to remember what
they owe): person/collective owed, quantity received, and "lo devolveré antes de __/20__".

What the paper captures that v1 does **not** yet:
- **Contact** of the party ("localizable en") — v1 stores only a free-text name. Digitally
  this becomes the **Nostr pubkey** (reachable by DM); keep a free-text contact for the
  offline/paper case.
- **Structured return options** instead of one free-text line: *similar quantity* (default),
  *equivalent work in N hours*, or *other*. The work-hours option is time-as-currency — it
  overlaps Tane's hand-over payment model, which already allows a time/`tiempo` currency
  ([project-sales]; €/Ğ1/time/none).
- **"Similar" is a defined term** — open-pollinated, non-GMO, organically grown — which ties
  to Tane's existing organic/eco self-declaration flag. The default return preset should
  carry this meaning, not just the word.
- **Return-by is month/year**, matched to cultivation cycles (v1 stores a full day; Phase 1's
  picker is day-precision — display can round to month/year).

## The goal

Turn the one-sided note into the paper Plantaré's **two matching signed stubs**: when two
people close an exchange, both devices end up holding the *same* commitment, each signed by
both, so either can prove "recibí X, devolveré una cantidad similar antes de la fecha Y".

Non-goals: interest, enforcement, debt framing (microcopy stays "promise, not debt");
a central server or global ledger (it stays device-to-device / relay-as-enrichment).

## Identity — counterparty becomes a real key

Replace (or rather, *augment*) the free-text `counterparty` with the counterparty's **Nostr
pubkey / `Party`**, reusing the identity already present in the social layer (offers, chat).
The flow starts from an existing conversation or a closed offer, so the pubkey is in hand.
Keep the free-text field as the offline/paper fallback (a Plantaré made face-to-face with
someone who has no app, or ingested from a paper form / QR).

## Schema delta (versioned Drift migration)

Add the deferred columns to `Plantares`, nullable so v1 rows coexist untouched:

- `debtorKey` TEXT — pubkey of who received and owes a return.
- `creditorKey` TEXT — pubkey of who gave.
- `debtorSignature` / `creditorSignature` TEXT — signatures over the canonical payload
  (both "stubs of the paper").
- `movementId` TEXT → `Movement` — the shared `given` event this accompanies (the
  `Movement.plantareId` link already exists on the other side).
- `remoteState` enum — `proposed` / `accepted` / `declined`, distinct from `status`
  (`open`/`returned`/`forgiven`), which tracks the *promise*, not the *handshake*.

From the paper form:
- ~~`contact` (the paper's "localizable en", email/phone)~~ — **decided NOT needed**
  (2026-07-13). It's a paper-era artifact: digitally the counterparty is a Nostr pubkey,
  reachable by DM, so a manual contact field is redundant. Offline/paper Plantarés keep it
  only inside the free-text `counterparty`.
- `returnKind` enum — `similar` / `workHours` / `other`, mirroring the form's checkboxes
  (default `similar`, meaning open-pollinated · non-GMO · organically grown — surface that
  definition, not just the word). `workHours` REAL for the "trabajo de N horas" option;
  `owedDescription` still backs `other`. Note the work-hours option overlaps Tane's hand-over
  time/`tiempo` currency — reconcile the two rather than duplicating. Kept for the Phase 2
  build; not added to v1.

A v1 local row = both keys/signatures null, `remoteState` null. Migration test in CI per
[`data-model.md` §5](data-model.md).

## Transport — `PlantareTransport` in `commons_core`

A new interface alongside `OfferTransport` / `MessageTransport` / `TrustTransport` /
`RatingTransport` / `SyncTransport` / `ReportTransport`
(`packages/commons_core/lib/src/social/`), on the shared `NostrConnection`. Unlike ratings
(public signed events), a Plantaré is **private to the two parties**, so it is *encrypted*,
like DMs/sync. Sketch, mirroring the existing `*Transport` shape:

```dart
abstract interface class PlantareTransport {
  /// Send a signed proposal (my signature only) to [counterpartyPubkey].
  Future<void> propose(PlantarePledge pledge);
  /// Counter-sign a received proposal and send the fully-signed copy back.
  Future<void> accept(PlantarePledge pledge);
  /// Decline a received proposal.
  Future<void> decline({required String pledgeId, String reason});
  /// Incoming proposals / accepts / declines addressed to this identity.
  Stream<PlantareEnvelope> incoming();
  Future<void> close();
}
```

**Handshake:** propose → accept → counter-sign. A proposes (signs, encrypts to B); B reviews,
accepts (adds B's signature), sends the doubly-signed copy back; both persist the identical
row (`remoteState = accepted`). Decline leaves A's row `declined`.

**Channel:** prefer **NIP-17 encrypted DM** (already used for messaging) carrying a canonical
JSON payload — least new surface, rides the existing connection. A dedicated event kind is an
alternative if we want first-class filtering. **Offline QR/NFC** device-to-device (PLAN.md's
favoured, most-decentralized path) is the same payload exchanged without a relay — the
transport abstracts channel from payload. Relays are enrichment only; offline must fully work.

## Provenance DAG

Each accepted Plantaré links parent→child (this lot came from that exchange), forming the
variety's distributed **procedence DAG** ([PLAN.md §5-bis](../../PLAN.md)) — "un grafo de
commits, pero de semillas". `movementId` + the counterparty's prior movement give the edge.

## Reputation anchor

An `accepted` Plantaré is the **strong anchor** for a rating (both parties provably closed a
real deal), upgrading the current soft, per-conversation anchoring — closing the
[`open-decisions.md`](open-decisions.md) / [`network-trust.md`](network-trust.md) item.

## Resolved in the v1 build (2026-07-14)

- **Conflict / divergence:** RESOLVED as anticipated — the two signatures cover the
  canonical core (everything except the signatures), and the `pledgeId` is part of that
  core. Any edit changes the signing hash, so an accept always closes the *exact* payload
  proposed; a changed pledge is a different id and must be re-proposed. `PlantareCrypto`
  enforces this; the "editing after signing invalidates the stub" test pins it.
- **Revocation / forgiveness sync:** kept **one-sided** for v1 — `status`
  (`open`/`returned`/`forgiven`) stays each party's own view, distinct from the shared
  `remoteState` handshake. Propagating a forgive to the counterparty is a follow-up (it'd
  need a signed "released" move).
- **Expiry:** `dueBy` passing is a **UI nudge only** (overdue styling), as in Phase 1 — it
  does not change `remoteState`.
- **Paper ingest:** the schema already supports it (nullable keys/signatures + free-text
  `counterparty`); no UI path yet — deferred.
- **Group/collective party:** deferred — still tied to the collective-identity question in
  `sharing-model.md` §6.

## Deferred to a follow-up

- **Durable send outbox.** Propose/accept send best-effort over the current session; if
  offline the local row is recorded but the move isn't queued for retry. A durable outbox
  (like `offer_outbox`) should back it so a proposal made offline goes out on reconnect.
- **Accept after restart before redelivery.** Pending proposals live in memory (refilled by
  NIP-17 redelivery on reconnect); accepting one the relay hasn't redelivered yet shows a
  gentle "offline / reconnecting" nudge rather than reconstructing the pledge from the row.
- **Rating anchor.** An `accepted` Plantaré is now a provable closed deal, but the rating
  flow isn't yet wired to require/prefer it (the "strong anchor" upgrade).
- **Provenance DAG.** `movementId` is stored; building the distributed procedence graph
  from accepted pledges is not done.
- **Forgiveness/paper/group** as above.
