Judge glance

Taskmaster. Gemini 3.5 Flash (Vertex AI) + Google ADK on Cloud Run. After one merchant confirm, the device finishes the day: Drift ledger → Drive backup → FIFO aging → ranked Gmail (PDF Top 5). The model plans; the phone commits. SMTP 250 is Gmail accept, not mailbox-delivered.

Inspiration

Shops that still run on a paper دفتر close the day by hand. Balances live in the merchant’s head. Collections wait until someone remembers who owes. That close-the-day chore is the friction this project exists to remove.

I built Daftar Closing Agent (وكيل إغلاق الدفتر) for the Taskmaster track of All Things Agentic: not a chatbot that talks about the ledger, but an agent that plans the day’s close and then takes action — backup, aging, ranked Gmail — after one merchant confirm.

The product is Arabic-first (RTL ar default, en second). Built-in money is YER, SAR, and USD. That is a paper-ledger shop problem, not a pan-regional coverage claim.

The long-running task is the shop’s business day, persisted on the phone. Each Cloud Run /run is fast on purpose. Cloud Run may scale to zero. That is the design, not a compromise: idle always-on agents are a poor fit for a merchant who is offline most of the afternoon.

What it does

Mid-day. The merchant speaks or types a capture (Mohamed owes 500 sugar). Gemini 3.5 Flash on Cloud Run (Google ADK) emits a structured proposal. The device shows a Confirm card. Money hits Drift only after the merchant taps confirm.

Close-the-day. One plan (propose_closing_plan). One tap: Confirm & Send Statements. The device then runs a deterministic ritual:

  1. Drift localDay summary (closing totals come from the ledger, not from the model)
  2. Google Drive backup upload
  3. FIFO aging rank of overdue accounts
  4. Collections Desk preview
  5. POST /v1/email/send-batchsmtp.gmail.com

Send set (device-ranked, not model-picked): up to 20 overdue contacts with a valid email. MIME PDF statements on the ranked Top 5. The remainder get a fixed text-only reminder (Appendix C.2). Gemini does not choose recipients and does not author each email.

Confirm without sending is first-class: the ritual still runs; send-batch is never called.

Proof of Action. Inbox PDF on a Top-5 contact + Cloud Logging daftar.agent.email with SMTP 250 and a unique Message-ID. SMTP 250 means Gmail accepted the message. It is not mailbox-delivered, not a read receipt, and not a DSN. Gmail SMTP has no delivery webhook — this project does not fake one.

How I built it

Custom FastAPI on Cloud Run (daftar-closing-agent, us-central1) hosts ADK POST /run (web=False) plus sibling routes POST /v1/email/send-batch and POST /v1/tts. No second agent runtime. No webhook service.

Layer Choice Why
Planning One ADK LlmAgent named closing_agent, model gemini-3.5-flash on Vertex AI Contest-mandatory Gemini 3.5+; one hop for intent-dependent mid-day capture
Tools Eight frozen FunctionTools — proposals only, no send-email tool Catalog freeze tests; Gemini must not move money or mail
Source of truth Flutter + Drift (SQLite) on device Survives airplane mode and Cloud Run scale-to-zero
Money amountMinor: int only No double / num on the wire or in Drift
Outreach Sibling FastAPI + Gmail SMTP Consent is the device button, not a tool call
Secrets Secret Manager file mount /secrets/gmail-smtp-app-password Never in Flutter, git, or chat
Auth Cloud Run IAM + ID tokens with custom OAuth audiences No allUsers
Voice out Cloud TTS Chirp 3 HD Enceladus (ar-XA / en-US) on POST /v1/tts Sibling route, not an ADK tool; flutter_tts is the offline fail-soft

Official Architectural Discipline (30% of the score — also how Best Architectural Design is awarded; it is not a track you select):

  • Decouple — Cloud Run plans; Drift commits; SMTP is a sibling route, not a FunctionTool.
  • State — Drift + device ClosingAgentPhase; scoped daftarContext (the ledger is not dumped into Gemini). No vector database — a debt ledger is a financial source of truth, not a RAG corpus.
  • Credentials — ID-token custom audiences; Gmail App Password in Secret Manager only.
  • Failures — confirm-gate lock per proposalId; Drive skip does not abort close; per-row SMTP failed / skipped; Cloud Run down leaves the ledger intact. Durable send key is device batchId / Drift.

I did not ship SequentialAgent or a multi-agent graph. Mid-day capture is one intent-dependent hop (parallel propose_*). The close ritual is device-deterministic after one plan confirm. Taskmaster scores a complete workflow, not an org chart of sub-agents.

Data sources. Merchant-entered ledger data on device (contacts, transactions, integer balances). Demo path: onboarding Try with Demo Store (7 USD overdue contacts). No third-party credit bureau, no scraped inboxes, no vector index of the books.

Disclosure. The offline Drift ledger, Drive Auth V2 backup, and Khazna UI shell are pre-existing substrate, disclosed in docs/CONTEST_DISCLOSURE.md. The agentic closing workflow (ADK + Gemini 3.5 + Cloud Run + HITL + Gmail SMTP + Architecture HUD) was built during the Submission Period (3–31 Aug 2026). Hybrid E (wa.me drafts) is a contest-period leftover, not substrate and not the filmed climax.

Challenges I ran into

HITL vs “no human intervention.” Taskmaster scoring asks whether a multi-step workflow completes without babysitting the agent. Money and outreach still need a merchant. The honest split: Gemini proposes; the merchant confirms once; after Confirm & Send Statements the device ritual and SMTP dispatch run without further prompts. Confirm-without-sending is the same ritual with mail skipped.

Scale-to-zero vs send idempotency. Cloud Run min instances = 0. A process-local idempotency map on the service is wiped when the revision sleeps. The durable send key therefore lives on the device (batchId / Drift), not in Cloud Run memory.

Integer money across locales. Spoken 500 is 500 minor units in YER (0 decimal places) and 50_000 cents in the USD demo fixture. A single ×100 bug would silently 100× every Yemeni debt. Tools refuse floats.

Vertex routing. gemini-3.5-flash 404s on a narrow regional Vertex endpoint. The service pins GOOGLE_GENAI_USE_VERTEXAI=TRUE and GOOGLE_CLOUD_LOCATION=global.

SMTP honesty. It is tempting to narrate 250 as “delivered.” I did not. The inbox screenshot is Proof of Action; the log line is accept + Message-ID.

Custom audiences. Cloud Run invokers use Google ID tokens whose audience is the OAuth client IDs, not “the service URL.” Getting that wrong looks fine in a happy-path demo and fails for real Android/iOS clients.

What I learned

  • The agent should be a clerk who proposes, not a cashier who writes the books. Confirm-before-commit is the architecture, not a UI flourish.
  • For an offline merchant, the phone is the system of record. Cloud is a planner and a mail worker that may disappear.
  • Frozen tools are cheaper to reason about than a growing toolbox. Eight FunctionTools, catalog-freeze tests, no ninth “just send it” tool.
  • Christina Lin’s pre-submission check-in (25 Aug 2026): fast /run is not a penalty. What counts is whether the user’s task spans the day — and whether the demo shows the work, not a chatbot recap.

Try it / docs

Repo. Private GitHub; shared with testing@devpost.com and cloudhackathons@google.com (Rules §6). Open the code repo field on this form (not the public blob links). Paths below are in that clone.

  • Spin-up: README.md
  • Architecture: docs/architecture/contest_architecture.png (same file as the required Devpost upload) · captions in docs/architecture/contest_architecture.md
  • Film script: docs/contest_demo.md
  • Cloud Run: IAM-authenticated (roles/run.invoker). Unauthenticated GET is 403. Use the Hosted project URL field on this form; do not treat it as a public demo.

Built With

Share this project:

Updates

Submission history