Inspiration

Official mail is written for the people who send it, not for the people who get it. Tadah is an iOS app for immigrants and families who get English letters, bills and notices they can't fully read. You scan the letter, and Tadah explains what it is, why you got it and what to do next, in English, Spanish or Simplified Chinese.

Tadah's checklists already included a script for what to say on the phone. What it couldn't do was make the call, and that's often the hardest step. For someone who reads English slowly, a phone call means a live conversation in English, a phone menu, hold music, and a stranger asking for an account number. It's the step people put off until the late fee arrives.

CALL-E made the next idea buildable: if Tadah already knows who to call and what to ask, it can offer to make the call itself.

What it does

  1. Understand. Scan a letter. The Review screen explains it and lists the steps.
  2. Offer. When a step needs a phone call, a card appears under the steps: Tadah can make this call, with the business, the number printed on the document, and a suggested question.
  3. Confirm. One short page: who Tadah will call (editable, because a scan can misread a digit), what it will ask (an editable draft), what it may agree to (Nothing by default), and whether to share the account number (off by default).
  4. Call. CALL-E's agent places the call in English and opens by saying it's an AI assistant calling on the user's behalf. The user is never on the line and can close the app.
  5. Report back. The same card changes in place to Call ended, showing what they said, what was agreed (a date, a reference number) and the next step, all saved on the to-do.

How I built it

  • iOS app: Swift, SwiftUI and SwiftData. Apple's Vision framework reads the letter on the device, and only the recognized text is sent for explanation, never the photo.
  • Backend: a Cloudflare Worker with a D1 database. It calls OpenAI to explain documents and is the only thing that talks to CALL-E, so no API key lives on the phone.
  • Placing a call: POST /actions checks the session, a verified email, the Plus subscription (via RevenueCat) and the monthly limit. Every check fails closed. It then records the call and sends CALL-E's POST /v1/calls:
    • a task built from the user's goal, the "may agree to" line and fixed rules
    • a JSON result_schema: outcome, detail, agreed, next_step, needs_user_action
    • an Idempotency-Key equal to our action id, so a network retry can never dial twice
  • Getting the answer: CALL-E's webhook isn't signed, so the Worker treats it as a doorbell. It arrives on a secret path, duplicate events are ignored, and the Worker fetches GET /v1/calls/{id} from CALL-E and trusts only that. The app picks up the result whether or not it stayed open.
  • Safety in code, not just the prompt: numbers are checked against US numbering rules, so 911 and other N11 codes, 900 and 976 are refused. Requests containing card numbers (Luhn check) or Social Security numbers are rejected by the server. Only one call can be live per user, and nothing in the call path retries automatically.
  • Testing: CALLE_DRY_RUN=1 runs the whole flow without dialling. The Worker has Vitest suites for the call logic and routes, and the app has unit tests for the call offer.

Challenges I ran into

  • Calling the right number. My first version pattern-matched phone numbers in the document text. On the first real bill I tested, a water bill, it offered the account number as the number to call, because any ten-digit run looks like a phone number. Now the model reads the whole page and returns the one number the document says to call. The app still validates it (a US exchange can't start with 0 or 1) and rejects anything that matches the document's own reference numbers.
  • The language of the call. The goal is written in the user's language, and an agent given a goal in Chinese could call a US utility in Chinese. The task now tells the agent to understand the goal in whatever language it's written in, then speak English for the whole call.
  • No time limit and no cancel button. CALL-E has no maximum-duration setting and no cancel endpoint, so every limit had to come from my design: the time rule lives in the task, a person confirms every call, only one call can be live, and nothing retries.
  • Failures that all looked the same. During testing my CALL-E credit balance ran out, and every later call failed with the same generic "couldn't place the call". It looked like a bug in my code. The Worker now saves CALL-E's refusal code on each call, so a failure can explain itself.
  • Where the offer belongs. I first planned to put it on the saved to-do, but nobody is guaranteed to reopen one. It now sits on the Review screen, right after the scan, when attention is highest. Saying "yes" also saves the to-do, so the call has somewhere to put its answer.

What I learned

  • Permission works best as data. The "may agree to" line is the only thing the agent can commit the user to, and it defaults to nothing.
  • Limits built into the system beat rules in a prompt. A prompt can ask for four minutes; only code can guarantee one live call, no retries and a human confirmation.
  • A webhook is a doorbell, not the delivery: always re-fetch from the source.
  • Design for the app being closed. The call runs on the server, and the answer waits on the to-do.
  • The language gap is the feature. The call has to be in English precisely because the user's language isn't.

What's next

Tadah 1.0 has been approved by App Review. Calls reaches everyone after account hardening, and the next step for it is returning the answer in the user's own language, not just the screens around it. After that comes acting online, such as checking a status or pre-filling a form, following the same loop: understand → decide → offer → report back.

Built With

  • apple-translation
  • apple-vision
  • call-e-api
  • cloudflare-d1
  • cloudflare-workers
  • ios
  • javascript
  • openai-api
  • rest
  • revenuecat
  • storekit
  • swift
  • swiftdata
  • swiftui
  • usernotifications
  • vitest
  • webhooks
  • wrangler
  • xcode
Share this project:

Updates

Submission history