Skip to main content

Work Submission Journey

How a piece of regenerative work moves from a gardener's phone to an EAS attestation, surviving offline conditions and operator review along the way. This is the highest-volume journey in Green Goods and the offline-first mechanic that drives the rest of the architecture.

Personas

  • A: Gardener — submits the work via PWA wizard or via the agent.
  • B: Operator — reviews queued submissions and signs the approval attestation.

State machine

Entry points

EntrySurfaceTrigger
Client PWApackages/client/src/views/Garden/index.tsxGardener taps "Submit work" on Home or deep-links from a notification
Telegram botpackages/agent/src/handlers/submit.tsFree-text or voice message in Telegram (no command needed)
Admin Hub (operator review)packages/admin/src/views/Hub/index.tsx (stage work)Operator opens Hub workspace, selects garden

Steps

Submission (Persona A)

#StatePersonaSurface (package + view)Hook / ServiceSide effectsStatus
1SelectingActionAclient / views/Garden/IntrouseWorkSelectionUI state onlyshipped
2AttachingMediaAclient / views/Garden/MediauseWorkFlowStore.audioNotes, useAudioRecordingMedia held in-memory + IndexedDB until uploadshipped
3EnteringDetailsAclient / views/Garden/DetailsuseWorkFormContext (RHF + Zod)Form stateshipped
4ReviewingAclient / views/Garden/ReviewuseOffline (gates queue messaging)None until tap "Upload Work"shipped
5SubmittingAclient (cross-cutting)useWorkMutation, useWorkMutationWithProgressIPFS pin via shared services; EAS attest(WorkSchema); emits workMutation.isPendingshipped
6Queued (offline)Aclient (cross-cutting)Job queue (packages/shared/src/modules/job-queue)Mutation persisted to IndexedDB; UI surfaces pendingCountshipped
7SyncedAclient (cross-cutting)useBatchWorkSync, useDraftAutoSaveJob queue flushes when online; toast on success/failureshipped
5aAgentText / AgentParsedAagent / handlers/submit.tsparseWorkText, locale-awareNone — confirmation step held in sessionshipped
6aAgentPendingAagent (db)db.addPendingWork, db.getOperatorForGarden, notifyOperatorDB row + Telegram message to operator with /approve <id> buttonshipped

Review (Persona B)

#StatePersonaSurface (package + view)Hook / ServiceSide effectsStatus
8AwaitingApproval (admin Hub)Badmin / views/Hub (stage work)useHubWorkbenchController, useReviewerWorks, HubWorkQueueRead EAS WorkApprovals + Envio Works via eas.ts and usePlatformStatsshipped
9ApprovedBadmin / views/Hub/components/HubWorkCarduseWorkApproval, useBatchWorkApproval, useWorkApprovalActionsEAS attest(WorkApprovalSchema). Resolver checks HatsModule.isOperator(attester)shipped
9bApproved (agent)Bagent / handlers/approve.tsblockchain.submitWork (gardener key), blockchain.submitApproval (operator key)Two attestations: work uses gardener custodial key, approval uses operator key — prevents self-attestation. auditLog("operator:approve", ...)shipped
10RejectedBadmin (Hub) or agent / handlers/reject.tsuseWorkApprovalActions, db.removePendingWorkGardener notified via Telegram; no on-chain attestation. auditLog("operator:reject", ...)shipped
11Attested(system)EAS GraphQLpackages/shared/src/modules/data/eas.ts (getWorkApprovals, getGardenAssessments)Work + WorkApproval indexed by easscan.org. Not indexed by Envio (per indexer schema comment lines 259-264)shipped

Failure / recovery paths

  • Network drop mid-submit. Garden/index.tsx reads isOnline from useOffline. Failed mutation is captured by useWorkMutation → enqueued via job queue. Review tab surfaces queue status: "You're offline. Your work will sync when you're back online." On reconnect, useBatchWorkSync flushes.
  • Draft persistence. useDraftAutoSave writes the in-progress form to IndexedDB on exit. useDraftResume re-prompts on next entry. The DraftDialog lets the gardener continue or start fresh.
  • Self-attestation guard (agent path). agent/handlers/approve.ts lines 82-106 explicitly use the gardener's custodial private key for the work attestation and the operator's key for the approval attestation. The on-chain resolver rejects work attested by the same address that signs the approval (work.attester != approval.attester).
  • EAS attest revert. parseContractError extracts the revert reason; mutation surfaces user-friendly text via USER_FRIENDLY_ERRORS. Job queue retains the job until the user explicitly discards.
  • AI parsing failure (agent path). parseWorkText returns tasks: []. Bot responds with examples and does not create a draft. No retries — gardener resubmits.
  • Operator permission denied (agent path). blockchain.isOperator(garden, user.address) returns {verified: false}. Bot replies with reason; no DB mutation.
  • Operator approves work for gardener with no account. db.getUser(pendingWork.gardenerPlatform, pendingWork.gardenerPlatformId) returns null. Bot replies "Gardener account not found. They may need to run /start first." This is a real failure mode in mixed-channel onboarding.

Connections

Notes for builders

  • The indexer does not index EAS attestations directly. Work and WorkApproval data is fetched from EAS GraphQL via packages/shared/src/modules/data/eas.ts. Do not add EAS schemas to the Envio config.
  • Gardener custodial keys (agent path) are stored in the agent's database, not on the blockchain. Treat them as secrets (audit-logged on every use).
  • The Hub stage work filters to pending submissions for the selected garden. A garden must be selected (via CanvasWorkspaceSelectionState) before the queue renders.
  • Hub FAB (buildHubFabConfig) gates the "Submit Work" action on canManage — operators can submit on behalf of a gardener via the admin Hub when needed.