Skip to main content
This chapter is the field guide. One section per service. Read it once and you can navigate any future architecture conversation about Guild.

The seven processes

Seven processes. Loopback inter-service traffic. nginx is the only thing listening on a public socket.

core-api

The brain. Identity, jobs, payments, KYC, wallet, dashboards, and the orchestrator of every OPay call.

What it owns

  • Identity schema — users, sessions, OTPs, KYC records (envelope-encrypted), virtual accounts, mandates, payout destinations.
  • Jobs schema — job postings, applications, attendance, reviews, escrow ledger entries.
  • Payments schema — wallets, transactions, payouts, bills, audit log.
  • OPay orchestration — every OPay call goes through @guild/opay-client, wrapped by service classes in core-api (opay-mandate, opay-payout, opay-va, opay-bills, opay-sms, etc.).

Entry points

Boot sequence

1

Validate config

Zod runs against process.env. Missing or malformed env crashes the process at boot, not at request 1,000.
2

Connect to Postgres via pgbouncer

Prisma client initialised with multi-schema mode. Read replicas (when configured) are routed automatically for read-only queries.
3

Connect to Redis

For sessions, idempotency cache, and BullMQ queues.
4

Initialise OPay client

Caller-injected config (the client never reads env directly). Single axios instance with 10s timeout. Circuit breaker armed.
5

Start express server

Listen on the loopback port. Pino HTTP logger attached. Request-ID middleware emits a ULID per request.
6

Workers

BullMQ workers start for outbound notifications, webhook retries, scheduled escrow releases.

Five-layer discipline

Every endpoint follows the same shape:
Dependencies flow downward only. Services don’t import controllers. Repositories don’t import services. This is enforced by import discipline, not by a framework.

Scaling characteristics

  • Cluster mode in PM2. Two instances by default; can scale to vCPU count.
  • Stateless. No local file writes that outlive the process. Sessions in Redis, queues in Redis, file uploads in R2.
  • Idempotent. Every write endpoint accepts an Idempotency-Key header.

whatsapp-svc

The WhatsApp brain. Inbound message handling, the 23-intent engine, Flows, voice-note interpretation, multi-language copy.

What it owns

  • WhatsApp schema — conversations, messages, conversation state machines (onboarding, KYC, payment flows).
  • Intent engine — deterministic intent routing across 23 intents in five languages.
  • Outbound message templates — pre-approved with Meta, fired through internal endpoints.
  • WhatsApp Flows — rich form-based UX inside the chat (KYC, job posting, withdrawal).

How a message flows

The 23 intents

The intent engine recognises 23 distinct conversational intents — wallet, withdraw, post a job, find work, my applications, complete profile, link bank, direct debit, buy airtime, buy data, pay electricity, link card, paid with card, link bank, cancel debit, show account, marketplace, my jobs, help, and so on. Routing is deterministic — instant classification, predictable cost, no LLM round-trip per message. When a message doesn’t match any intent, the conversation state machine takes over. If the user is mid-flow (e.g., halfway through a withdrawal), we keep them in the flow. If they’re idle, we show the home menu.

Voice-note interpretation

Every voice note received in WhatsApp is dropped into the queue. Our transcription model returns text. The text runs through the intent engine the same way a typed message would. End user gets a typed reply — they spoke, the system understood, and they didn’t have to type a single character.

Why a microservice and not part of core-api

Three reasons.
  1. Meta requirements. Webhook verification has its own signature scheme. Isolating the WhatsApp endpoint from the rest of the API reduces blast radius.
  2. State machine complexity. Conversation state lives best near the channel that owns it. A schema migration in core-api shouldn’t ripple through WhatsApp flows.
  3. Independent scaling. WhatsApp traffic spikes (e.g., a marketing campaign) shouldn’t queue up against payment traffic.

voice-svc

Tola, our voice agent. Inbound calls, outbound calls, and the tool-call dispatcher that maps Tola’s intents to core-api actions.

What it owns

  • Voice schema — calls, transcripts (when captured), tool-call records, outbound campaign queue.
  • Tool dispatcher — maps voice-agent tool calls (sign up, list jobs, get details, apply, update profile, set skills) to authenticated core-api requests on behalf of the caller.
  • Outbound call orchestration — when our matching model wants to nudge a high-fit candidate, voice-svc places the call.

How an inbound call flows

The caller-resolver

Before Tola speaks, voice-svc looks up the caller’s phone number in identity. Three outcomes:

Outbound calls

Voice-svc can place outbound calls too. The most common use case: when the matching model surfaces a high-fit Job Seeker for a new job (Trusted tier, strong recent activity, matching skill and location) and we want to nudge them faster than a WhatsApp message would.
Tola opens with the language the user set in their profile, briefs them on the job, and offers to apply on their behalf.

sms-svc

The thin bridge. Sits between core-api and our SMS gateway hardware. Doesn’t own any business logic.

What it owns

  • SMS schema — outbound message log, delivery receipts.
  • Routing — outbound SMS request from core-api → through sms-svc → to the right backend.

Two outbound backends

sms-svc routes SMS by purpose. The split lets us use the right tool for the right purpose. Transactional one-way uses OPay’s high-volume API. Conversational two-way uses physical SIM cards we control.

Why a separate service

sms-svc is intentionally thin. If we ever migrated SMS providers (e.g., split traffic across two SMS APIs based on delivery region), the rewrite happens here without touching core-api. See SMS infrastructure for the gateway side.

ml-service

The Python service. FastAPI. Four models, each behind its own endpoint.

What it owns

  • ML schema — model artifacts metadata, feature snapshots for offline training, predictions log (when persisted), bandit state in Redis.
  • Standing model — multi-class classifier predicting Job Seeker / trader tier (Building, Emerging, Trusted, Established) from ledger features.
  • Matching model — lambdarank ranker for job ↔ Job Seeker pairs. Four factors: skill, location, language, economic context.
  • Negotiation engine — Thompson sampling Beta(α, β) bandit per (segment × bucket). Online learner; posteriors update on every accept/decline.
  • Transcription model — voice-note interpretation. Five languages: English, Pidgin, Yoruba, Igbo, Hausa.

Boot

FastAPI loads each model from its artifact path at startup. A boot-time feature-order sanity check confirms the loaded model’s feature schema matches what the application expects. Mismatch crashes the process so production never silently serves stale features.

Why Python, not Node

The tooling. xgboost, lightgbm, scikit-learn, and the transcription ecosystem are Python-native. We use what’s mature.

Why a separate service, not a Python module pulled into core-api

Process isolation. Models reload independently from API code. Heavy inference doesn’t compete with payment latency. Two different deployment cadences. See ML layer for the model deep dive.

sms-server (Go SMS gateway)

A sibling process — same VPS, different runtime. Open-source (capcom6/sms-gateway), customized for our deployment.

What it does

Pairs with one or more Android phones connected over USB or networked over LAN. Each Android phone has a Nigerian SIM. The Go server exposes a RESTful API on a loopback port; when sms-svc wants to send a conversational SMS, it POSTs to the gateway. The gateway dispatches via Firebase Cloud Messaging to the paired Android, which uses the SIM to send the SMS. Replies go the reverse path: SIM receives an SMS → Android relays to the gateway → gateway pushes inbound event to sms-svc → sms-svc forwards to core-api.

Why hardware instead of an API

Three reasons.
  1. Two-way deliverability. Nigerian SMS APIs are excellent for one-way transactional traffic; two-way conversational is messier. Owning the SIM gives us guaranteed two-way.
  2. Cost. Per-message cost on a SIM card is fractional. At scale, this beats any third-party SMS API for two-way traffic.
  3. Direct relationships with carriers. Each SIM is on MTN, GLO, Airtel, or 9Mobile. We control the rotation. We control the deliverability footprint.
This is one of the boring-but-strategic parts of the stack. Most platforms outsource SMS entirely; we own the rails for the part that matters. See SMS infrastructure for the deployment detail.

sip-server (Asterisk)

A second sibling process. Asterisk on TLS 5061, dedicated to WhatsApp Calling.

What it does

When a Guild user opens our WhatsApp contact and taps the call button, Meta’s WhatsApp Calling infrastructure initiates a SIP INVITE to sip.guild.com.ng:5061. Asterisk accepts the TLS call, transcodes Meta’s Opus codec to G.711 PCMU (the format our voice runtime accepts), and bridges to Tola via SDES-SRTP.

Why Asterisk specifically

We tried three alternatives before settling on Asterisk. The path to “this works”:
  1. Meta dials Asterisk over TLS 5061 with Opus.
  2. Asterisk’s chan_pjsip accepts and identifies the endpoint by source IP.
  3. Asterisk transcodes Opus to PCMU (drops Meta’s tricky Opus fmtp params).
  4. Asterisk bridges to the voice runtime via SDES-SRTP.
  5. Tola is on the other side.

Why this matters

WhatsApp Calling lands directly on Tola. A user doesn’t need to dial a separate number; they tap the call button in their existing WhatsApp chat with Guild and they’re talking to her. Most platforms don’t have this. We do. See SIP infrastructure for the deeper protocol detail.

Shared @guild/* packages

Every service consumes from the same package set. Pinned versions across the monorepo. Sixteen packages. Each one solves a problem so the four engineers building features don’t reinvent it.

OPay integration

The 23 wrapped surfaces in detail. The reconcile guard. The five demo paths.

ML layer

Four models. Why each has the shape it does.

Infrastructure

VPS, CloudPanel, nginx, PM2, deploy.

Inter-service comms

HMAC svc-auth in depth.