# Using Meta AI via WhatsApp for OpenCode

> Source: <https://github.com/amita-seal/wa-metaai>
> Published: 2026-08-01 21:10:44+00:00

OpenAI-compatible endpoint backed by Meta AI, reached over WhatsApp. Lets opencode (or any OpenAI-compatible client) use Meta AI as a model.

``` php
opencode --> POST localhost:8788/v1/chat/completions --> whatsmeow --> WhatsApp
                                                     <-- Meta AI (867051314767696@bot)
```

Meta AI is not a phone-number contact. It is a **bot JID**: `867051314767696@bot`

. Delivery to a bot
requires two things on the outgoing stanza:

- a
`<bot>`

node appended to the stanza content - an HKDF-derived
`BotMessageSecret`

(`applyBotMessageHKDF`

over the message secret)

Baileys implements neither — its entire send path references bot JIDs once, only to skip issuing a
TC token. The observable symptom is precise and misleading: WhatsApp **server-acks** the message
(`status: 2`

) and then never delivers it, so it sits at one grey tick forever and Meta AI never
replies. A control send to a human JID on the same code path went PENDING → 2 → 3 → 4 (read),
proving the transport was fine and only bot support was missing.

whatsmeow has it: `types.NewMetaAIJID`

is exactly `867051314767696@bot`

, `send.go`

sets `isBotMode`

when the target `IsBot()`

, derives the bot secret, and attaches the `<bot>`

node.

Baileys' `META_AI_JID = 13135550002@c.us`

is legacy and unroutable — sending to it produces
`USync fetch yielded no results for pending PNs`

, WhatsApp's way of saying that number isn't a user.

The account itself must live in a real WhatsApp mobile app (an Android emulator works); this service
attaches as a **linked device** and cannot hold the registration on its own.

```
CGO_ENABLED=1 go build -o wametaai .
WA_PHONE=<digits, country code first> ./wametaai
```

First run prints a pairing code — enter it under **WhatsApp > Linked devices > Link with phone
number**. The session persists in `wametaai.db`

, so later runs need no code.

`~/.config/opencode/opencode.json`

:

```
{
  "provider": {
    "whatsapp": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "WhatsApp (Meta AI)",
      "options": { "baseURL": "http://localhost:8788/v1", "apiKey": "unused" },
      "models": {
        "meta-ai": { "name": "Meta AI (WhatsApp)", "tool_call": true, "limit": { "context": 8000, "output": 4000 } }
      }
    }
  }
}
```

Then `opencode run --model whatsapp/meta-ai "..."`

.

Meta AI has no native function calling, so the shim supplies it. When a request carries `tools`

,
`toolPrompt`

renders each tool's name, description and JSON Schema into the prompt and asks for a
single fenced object:

```
{"tool": "<name>", "args": {...}}
```

`parseToolCall`

extracts that — tolerating the fence, a language tag, and the surrounding chatter
Meta AI likes to add — and the reply is returned as a real OpenAI `tool_calls`

message with
`finish_reason: "tool_calls"`

, in both the JSON and SSE paths. Prior turns are replayed as
`[ASSISTANT ran tool]`

and `[TOOL RESULT]`

so it can see what already ran.

Verified working through opencode's real agent loop:

``` bash
$ opencode run --model whatsapp/meta-ai "Read the file notes.txt and tell me the secret word."
> build · meta-ai
→ Read notes.txt
pomegranate
```

Observed: opencode's full system prompt plus its tool schemas came to ~37,000 characters (~9k tokens) per request, and Meta AI handled it without complaint, correctly resolving a relative path to an absolute one. Two round trips, ~6s each.

**The ceiling to watch:** a single WhatsApp text message caps out around 65k characters, and a
trivial one-file task already used 37k. Larger context — more files, longer histories — will hit that
wall, and there is currently no truncation guard, so an oversized prompt fails rather than degrades.

| tool-call reliability | prompted, not native. Meta AI is a consumer assistant, so it can answer in prose where a tool call was wanted; the loop stalls on that turn rather than recovering. |
| streaming | reply is buffered then emitted as one chunk. Meta AI answers as many separate messages (~10 message IDs per answer), not edits to one, so they are joined in arrival order and considered complete after `WA_QUIET_MS` of silence. |
| system prompt | folded into a flattened transcript; there is no separate system role |
| conversation state | one WhatsApp thread with its own memory, so history is resent each turn and Meta AI's own recall can bleed across requests |
| concurrency | serialized — a single thread cannot serve parallel requests |
| token usage | estimated at ~4 chars/token; WhatsApp reports none |
| latency | ~7s for a short answer |

| var | default | |
|---|---|---|
`WA_PORT` |
`8788` |
HTTP port |
`WA_PHONE` |
— | required only for first-time pairing |
`WA_QUIET_MS` |
`4000` |
reply considered complete after this much silence |
`WA_TIMEOUT_MS` |
`120000` |
hard cap per request |

Registered on a rented TextVerified number, so the account lives only as long as that rental — a released number can be re-rented and re-registered by someone else, which would evict this account. WhatsApp also expires linked devices when the primary stays offline, so keep the emulator reachable. Unofficial-client use is against WhatsApp's terms and carries a ban risk for the number.
