The job we never filled
My company Supplya is a B2B procurement platform for Nigerian retailers. We posted a role: Customer Success Executive, ₦100,000 a month. The responsibilities were one screen long — call every new signup within 24 hours, walk them through their first order, find out why they joined.
We never filled it. So most of our signups were never called at all. They signed up, saw a dashboard they didn't understand, and left. We had no idea why, because the only person who would have known was the hire we didn't make.
Cierge is that hire.
What it does
A customer signs up. Cierge phones them, has a real two-minute conversation — who are you, what do you sell, what are you trying to fix — and turns that call into a row of structured data: business type, goal, pain points, sentiment, activation status, whether they want a human to call back. If they do, it files the task. If nobody picked up, it says so and queues a retry.
That last sentence is the whole product, and it took me the longest to get right.
How I built it
One Next.js 16 app on Vercel, Supabase for Postgres, and CALL-E for the voice layer.
Signup ──▶ POST /api/webhooks/supplya (shared-secret auth)
└─ upsert customer, insert call row, ask CALL-E to dial
CALL-E ──▶ POST /api/webhooks/calle (HMAC-verified)
└─ write transcript + insight + at most one follow-up
Dashboard ◀── Voice-of-Customer table
The conversation script and the extraction schema live in one file, so the prompt and the JSON contract can't drift apart. CALL-E does the hard part — dialling, turn-taking, and pulling structured fields out of natural speech. On the call that finally worked end to end, a grocery retailer talked for 1 minute 42 seconds across 15 turns, and this came back with a completion confidence of 0.94:
{ "business_type": "Retail Grocery",
"goal": "Get more people to buy from the business, especially online buyers.",
"pain_points": ["Wants to increase online orders, not just offline sales."],
"sentiment": "Positive",
"activation_status": "NeedsHumanFollowUp",
"wants_human_contact": true }
Nobody typed any of that. She just said it, on the phone, in her own words.
The challenges
CALL-E doesn't dial Nigeria. Neither does most of the voice ecosystem — the supported list is US, Canada, Australia, India, Singapore, Malaysia, UAE, Netherlands, Poland, Bangladesh, China. My customers are in Lagos. I bridged it: CALL-E dials a US Twilio number, which forwards to +234 over TwiML. It works, and it is the honest answer to why so few voice products serve this market.
A call that ends is not a call that was answered. This is the bug that taught me the
most. CALL-E returns status: completed when the call is over — not when a human spoke.
Voicemail completes. A ringing phone nobody picks up completes. My dashboard cheerfully
rendered those as green successes with no follow-up, which meant a customer who was never
reached looked exactly like a customer who had been onboarded. For a tool whose entire job
is telling you what happened to your signups, that is the worst possible failure.
It got worse when I looked at the transcripts. Nigerian carrier announcements — "All circuits are busy now" — arrive from the speech pipeline labelled as the customer speaking. The transcript is evidence, never truth.
So reachability is now decided from observed call evidence only, and the rules are narrow on purpose: a provider signal that a human answered, or speech that isn't carrier audio. Not turn count. Not "we got a result back", because extraction can fail on a real conversation and succeed on a recorded message. When the system genuinely can't tell what happened, it says needs review and refuses to redial — because "we don't know" and "nobody answered" are different facts, and only one of them gives you permission to phone someone again.
I nearly re-architected the whole thing on one data point. A call at 1:41 AM failed, Twilio logged "No Answer", and I concluded Nigerian carriers were permanently blocking us. I rebuilt the call flow as a conference bridge — which quietly inverted the product, since it required the customer to dial in. Two calls two minutes apart the next day told the real story: 12:35 connected and had a full conversation, 12:37 hit congestion. It was never a block. It was intermittent, and the correct response was a retry policy, not a rewrite. I reverted it.
What I'm proud of
Every hard-won fix in this repo is the same instinct: don't let a failure look like a success. "Reached" instead of "Completed" on the overview. An explicit banner on a call that never connected. Reconciliation that replays lost webhooks through the same classifier, so a recovered call and a live one are judged identically. One non-terminal call per customer, enforced by a database constraint rather than hope.
I wrote that doctrine up as a skill and opened it against CALL-E's own awesome-phone-call-agents repo. It went through five rounds of review and was merged.
What's next
Reactivation and feedback calls. CRM connectors. Follow-ups that dispatch themselves instead of waiting for a human to click. And a Nigerian carrier route, so the bridge can go away.
Built With
- call-e
- google-gemini
- next.js
- node.js
- postgresql
- react
- supabase
- tailwind-css
- twilio
- typescript
- vercel
- webhooks
- zod
Log in or sign up for Devpost to join the conversation.