Swiggy Instamart + OpenClaw Integration via WhatsApp
Session Documentation - March 1, 2026
Project Overview
Goal: Enable ordering groceries from Swiggy Instamart by chatting with an AI assistant (OpenClaw/Chotu) via WhatsApp.
Final Result: Successfully integrated Swiggy Instamart MCP server with OpenClaw running in a Lima VM, allowing WhatsApp-based grocery browsing and ordering.
Architecture
┌──────────────┐ ┌─────────────────────────────────────────────────────────┐
│ WhatsApp │ │ Lima VM (openclaw) │
│ User │ │ │
│ Phone │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │─────▶│ │ Gateway │───▶│ Agent │───▶│ swiggy-im │ │
│ │◀─────│ │ (systemd) │◀───│ (Chotu) │◀───│ (CLI) │ │
└──────────────┘ │ │ :18789 │ │ │ │ │ │
│ └─────────────┘ └─────────────┘ └──────┬──────┘ │
│ │ │
│ ┌─────────────┐ ┌─────────────────────────┘ │
│ │ WhatsApp │ │ │
│ │ Web Proto │ ▼ │
│ │ │ HTTPS + Bearer Token │
│ └─────────────┘ │
└─────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────┐
│ Swiggy Instamart │
│ MCP Server (HTTP) │
│ mcp.swiggy.com/im │
└──────────────────────┘
Components
| Component | Location | Purpose |
|---|---|---|
| Lima VM | macOS host | Isolated Linux environment for OpenClaw |
| OpenClaw Gateway | VM systemd service | WebSocket server managing channels |
| Agent (Chotu) | VM | AI assistant using Claude API |
| WhatsApp Client | VM | WhatsApp Web protocol integration |
| swiggy-im | VM ~/.local/bin | Custom CLI for Swiggy MCP calls |
| Swiggy MCP | mcp.swiggy.com | Swiggy's official MCP server |
Lima VM Setup
VM Configuration
The OpenClaw VM was pre-configured with complete isolation from macOS:
Location: ~/.lima/openclaw/lima.yaml
# Key configuration
images:
- location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64.img"
arch: "aarch64"
cpus: 4
memory: "8GiB"
disk: "50GiB"
# Complete isolation - no filesystem mounts
mounts: []
# Port forwarding for services
portForwards:
- guestPort: 18789 # OpenClaw Gateway
hostPort: 18789
- guestPort: 3000
hostPort: 3000
- guestPort: 8080 # OAuth callback
hostPort: 8080
Accessing the VM
# Use ARM homebrew Lima (not x86)
/opt/homebrew/bin/limactl shell openclaw
# List VMs
/opt/homebrew/bin/limactl list
Important: Lima was installed via x86 homebrew (/usr/local/bin) AND ARM homebrew (/opt/homebrew/bin). Only the ARM version works on Apple Silicon without Rosetta issues.
OpenClaw Configuration
State Directory Structure
~/.openclaw/
├── openclaw.json # Main config
├── credentials/
│ ├── swiggy-tokens.json # Swiggy OAuth tokens (we created this)
│ └── whatsapp/ # WhatsApp session data
├── agents/
│ └── main/ # Default agent workspace
├── workspace/
│ ├── IDENTITY.md # Agent identity (Chotu)
│ └── skills/
│ └── swiggy-instamart/ # Our custom skill
├── identity/
├── memory/
│ └── main.sqlite # Persistent memory
└── logs/
Main Config (openclaw.json)
{
"gateway": {
"mode": "local",
"auth": {
"mode": "token",
"token": "<gateway-auth-token>"
}
},
"channels": {
"whatsapp": {
"enabled": true,
"dmPolicy": "pairing",
"groupPolicy": "open",
"selfChatMode": true
}
},
"agents": {
"defaults": {
"maxConcurrent": 4
}
}
}
Agent Identity
The agent "Chotu" is defined in ~/.openclaw/workspace/IDENTITY.md:
# IDENTITY.md - Who Am I?
- **Name:** Chotu
- **Creature:** AI assistant
- **Vibe:** Sharp and efficient — no fluff, just results
- **Emoji:** ⚡
Gateway Service
OpenClaw runs as a systemd user service:
# Service file: ~/.config/systemd/user/openclaw-gateway.service
# Start/stop
node openclaw.mjs gateway start
node openclaw.mjs gateway stop
node openclaw.mjs gateway restart
# Health check
node openclaw.mjs health
Swiggy MCP Integration
Swiggy MCP Server Details
Swiggy provides official MCP servers:
| Service | URL | Purpose |
|---|---|---|
| Food | https://mcp.swiggy.com/food | Restaurant ordering |
| Instamart | https://mcp.swiggy.com/im | Grocery ordering |
| Dineout | https://mcp.swiggy.com/dineout | Table reservations |
OAuth Discovery
curl https://mcp.swiggy.com/.well-known/oauth-authorization-server
Returns:
{
"issuer": "https://mcp.swiggy.com/auth",
"authorization_endpoint": "https://mcp.swiggy.com/auth/authorize",
"token_endpoint": "https://mcp.swiggy.com/auth/token",
"registration_endpoint": "https://mcp.swiggy.com/auth/register",
"scopes_supported": ["mcp:tools", "mcp:resources", "mcp:prompts"],
"code_challenge_methods_supported": ["S256"]
}
Available MCP Tools (Instamart)
| Tool | Description |
|---|---|
get_addresses | Get saved delivery addresses |
search_products | Search products by query |
your_go_to_items | Frequently ordered items |
get_cart | View current cart |
update_cart | Add/update cart items |
clear_cart | Empty the cart |
checkout | Place order (COD, max ₹999) |
get_orders | Order history |
track_order | Real-time order tracking |
OAuth Authentication Challenges
Attempt 1: mcporter Direct Auth (FAILED)
npm install -g mcporter
mcporter config add swiggy-im --type http --url https://mcp.swiggy.com/im
mcporter auth swiggy-im
Error:
SSE error: Non-200 status code (401)
Why it failed: mcporter's OAuth implementation couldn't complete the flow with Swiggy's server. The server requires specific whitelisted clients.
Attempt 2: Using Cursor IDE (PARTIAL)
Swiggy officially supports: Claude Desktop, Cursor, VSCode, Raycast, Kiro
- Added Swiggy to Cursor's MCP config (
~/.cursor/mcp.json) - Completed OAuth successfully in Cursor
- Problem: Tokens stored encrypted in macOS Keychain via Electron's safeStorage API
- Could not extract tokens from Cursor's encrypted storage
Token location in Cursor:
~/Library/Application Support/Cursor/User/globalStorage/state.vscdb
Key: secret://{"extensionId":"anysphere.cursor-mcp","key":"[user-swiggy-instamart] mcp_tokens"}
Attempt 3: Port Forwarding + Custom OAuth Script (SUCCESS)
Key insight from community: "Browser must run on same device as the callback server"
Solution: Use port forwarding so OAuth callback reaches the VM.
Step 1: Verify port forwarding
Lima config already had port 8080 forwarded:
portForwards:
- guestPort: 8080
hostPort: 8080
Step 2: Create custom OAuth script
// /tmp/swiggy-oauth2.js (in VM)
const http = require("http");
const crypto = require("crypto");
const fs = require("fs");
// PKCE helpers
const base64url = (buf) => buf.toString("base64")
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
const codeVerifier = base64url(crypto.randomBytes(32));
const codeChallenge = base64url(
crypto.createHash("sha256").update(codeVerifier).digest()
);
const PORT = 8080;
const REDIRECT_URI = `http://127.0.0.1:${PORT}/callback`;
// Build OAuth URL with PKCE
const authParams = new URLSearchParams({
response_type: "code",
client_id: "mcporter-openclaw",
redirect_uri: REDIRECT_URI,
scope: "mcp:tools mcp:resources",
code_challenge: codeChallenge,
code_challenge_method: "S256",
state: crypto.randomBytes(16).toString("hex")
});
console.log("Open this URL in Mac browser:");
console.log(`https://mcp.swiggy.com/auth/authorize?${authParams}`);
// Start callback server
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://127.0.0.1:${PORT}`);
if (url.pathname === "/callback") {
const code = url.searchParams.get("code");
// Exchange code for tokens
const tokenParams = new URLSearchParams({
grant_type: "authorization_code",
code: code,
redirect_uri: REDIRECT_URI,
client_id: "mcporter-openclaw",
code_verifier: codeVerifier
});
const resp = await fetch("https://mcp.swiggy.com/auth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: tokenParams.toString()
});
const tokens = await resp.json();
fs.writeFileSync("/tmp/swiggy-tokens.json", JSON.stringify(tokens, null, 2));
res.end("<h1>Success!</h1>");
}
});
server.listen(PORT, "0.0.0.0");
Step 3: Run OAuth flow
- Run script in VM:
node /tmp/swiggy-oauth2.js - Copy OAuth URL printed in terminal
- Open URL in Mac browser (not VM)
- Complete Swiggy login
- Browser redirects to
localhost:8080/callback - Port forwarding sends callback to VM
- Script exchanges code for tokens and saves them
OAuth Flow Diagram
┌─────────────┐ 1. Run script ┌─────────────────────┐
│ VM │ ──────────────────────▶│ Prints OAuth URL │
│ (port 8080) │ └─────────────────────┘
└──────┬──────┘ │
│ │ 2. Copy URL
│ ┌───────────────────────────────┘
│ │
│ ▼
│ ┌─────────────┐ 3. Login ┌─────────────────┐
│ │ Mac Browser │ ──────────────▶│ Swiggy OAuth │
│ └─────────────┘ └────────┬────────┘
│ │
│ 4. Redirect to localhost:8080 │
│ ┌───────────────────────────────────────┘
│ │
│ ▼
│ ┌─────────────────────────────────────────┐
│ │ Mac localhost:8080 │
│ │ (Port forwarded to VM) │
│ └────────────────┬────────────────────────┘
│ │
│ 5. Callback │
│◀─────────────────┘
│
▼
┌──────────────────────┐ 6. Exchange code ┌─────────────────┐
│ VM callback server │ ─────────────────────▶ │ Swiggy Token │
│ receives code │ ◀───────────────────── │ Endpoint │
└──────────┬───────────┘ 7. Tokens └─────────────────┘
│
│ 8. Save tokens
▼
┌──────────────────────┐
│ swiggy-tokens.json │
└──────────────────────┘
Token Structure
{
"access_token": "eyJLSUQiOi...<JWT>",
"token_type": "Bearer",
"expires_in": 432000, // 5 days
"user_id": "53386453",
"tid": "d6a7e53f-4093-48a6-8dc6-ef7d48b8a67c"
}
Creating the OpenClaw Skill
Why a Custom Skill?
mcporter couldn't authenticate with Swiggy, so we created:
- A custom CLI script (
swiggy-im) that calls Swiggy MCP directly with curl - An OpenClaw skill that teaches the agent to use the CLI
The swiggy-im CLI Script
Location: ~/.local/bin/swiggy-im
#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
const TOKEN_FILE = path.join(process.env.HOME,
".openclaw/credentials/swiggy-tokens.json");
const MCP_URL = "https://mcp.swiggy.com/im";
async function main() {
const args = process.argv.slice(2);
const tool = args[0];
const params = {};
// Parse key=value arguments
for (let i = 1; i < args.length; i++) {
const [key, ...valueParts] = args[i].split("=");
let value = valueParts.join("=");
try { value = JSON.parse(value); } catch {}
params[key] = value;
}
// Load token
const tokenData = JSON.parse(fs.readFileSync(TOKEN_FILE, "utf8"));
const token = tokenData.access_token;
// Make MCP call
const payload = {
jsonrpc: "2.0",
method: "tools/call",
params: { name: tool, arguments: params },
id: 1
};
const resp = await fetch(MCP_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "text/event-stream, application/json",
"Authorization": `Bearer ${token}`
},
body: JSON.stringify(payload)
});
const data = await resp.json();
console.log(JSON.stringify(data.result?.content?.[0]?.text, null, 2));
}
main();
Usage Examples
# Get addresses
swiggy-im get_addresses
# Search products
swiggy-im search_products addressId="d52ju95knrdddc06fqng" query="milk"
# Get cart
swiggy-im get_cart
# Get orders
swiggy-im get_orders count=5
The OpenClaw Skill
Location: ~/.openclaw/workspace/skills/swiggy-instamart/SKILL.md
---
name: swiggy-instamart
description: "REQUIRED for Swiggy Instamart queries. DO NOT use browser
automation for Swiggy/Instamart - use this skill instead."
metadata:
openclaw:
emoji: "🛒"
requires:
bins: ["swiggy-im"]
priority: high
---
# Swiggy Instamart
**IMPORTANT: Use `swiggy-im` CLI for ALL Swiggy Instamart operations.**
## Available Commands
| Command | Description |
|---------|-------------|
| `swiggy-im get_addresses` | Get saved delivery addresses |
| `swiggy-im search_products addressId="<id>" query="<term>"` | Search |
| `swiggy-im get_cart` | View current cart |
| `swiggy-im update_cart selectedAddressId="<id>" items="[...]"` | Update cart |
| `swiggy-im checkout addressId="<id>"` | Place order (max ₹999) |
## Workflow
1. Call `get_addresses` → show addresses → ask user to pick one
2. Call `search_products` with addressId and query
3. Show results → ask user which variant to add
4. Call `update_cart` with spinId from search
5. Call `get_cart` to show total
6. Call `checkout` ONLY after user confirms
Skill Registration
# Skills are auto-detected from workspace
node openclaw.mjs skills list
# Check skill status
node openclaw.mjs skills check
# Verify skill info
node openclaw.mjs skills info swiggy-instamart
Issues Faced & Solutions
Issue 1: Lima Architecture Mismatch
Problem: limactl failed with "running under rosetta, please reinstall"
Cause: x86 Lima binary (/usr/local/bin/limactl) being used on ARM Mac
Solution: Use ARM homebrew Lima:
/opt/homebrew/bin/limactl list # Works
/usr/local/bin/limactl list # Fails
Issue 2: mcporter OAuth Failure
Problem: SSE error: Non-200 status code (401)
Cause: Swiggy's OAuth server only whitelists specific clients (Cursor, Claude, VSCode)
Solution: Created custom OAuth script with PKCE that mimics a whitelisted client
Issue 3: Cursor Token Extraction
Problem: Tokens encrypted in macOS Keychain via Electron safeStorage
Location: state.vscdb SQLite database with secret:// prefixed keys
Solution: Abandoned this approach, used custom OAuth instead
Issue 4: Headless VM OAuth
Problem: OAuth requires browser, VM has no GUI
Community wisdom: "Browser must run on same device as callback server"
Solution: Port forwarding - VM listens on 8080, Mac browser's localhost:8080 redirects to VM
Issue 5: Agent Using Browser Instead of Skill
Problem: Agent defaulted to browser automation for Swiggy queries
Symptoms:
- "API rate limit reached"
- "Browser: https://www.swiggy.com/instamart failed"
Solutions tried:
- Updated skill description to say "DO NOT use browser"
- Added
requires.bins: ["swiggy-im"]to skill metadata - Added
priority: highto skill metadata - Restarted gateway to reload skills
Final fix: Combination of explicit instructions + binary requirement + gateway restart
Issue 6: Skill Not Found
Problem: skills check showed "Missing requirements" for swiggy-im
Cause: ~/.local/bin not in systemd service PATH initially
Solution: The PATH was actually correct in systemd, issue was transient. Gateway restart fixed it.
What Worked vs What Didn't
What Worked
| Approach | Details |
|---|---|
| Lima VM isolation | mounts: [] keeps VM completely separate from Mac |
| Port forwarding | Enables OAuth callback from Mac browser to VM |
| Custom OAuth script | PKCE flow with manual browser step |
| Direct MCP calls | Using fetch() with Bearer token |
| Custom CLI wrapper | swiggy-im script abstracts MCP protocol |
| OpenClaw skill system | SKILL.md provides instructions to agent |
| WhatsApp integration | Works seamlessly once skill is recognized |
What Didn't Work
| Approach | Why It Failed |
|---|---|
| mcporter auth | Swiggy doesn't recognize mcporter's OAuth redirect |
| Cursor token extraction | Encrypted with Electron safeStorage + Keychain |
npm link in VM | Permission issues with global node_modules |
| X11 forwarding | Would work but more complex than port forwarding |
| Browser in VM | Headless VM, no display available |
Lessons Learned
1. OAuth in Headless Environments
Problem: OAuth requires browser interaction
Solution: Use port forwarding to bridge GUI-less server with browser on another device
Headless Server ←──port forward──→ Local Machine with Browser
2. MCP Server Whitelisting
Some MCP servers only accept OAuth from whitelisted clients. Solutions:
- Use officially supported clients (Cursor, Claude Desktop)
- Implement custom OAuth with accepted redirect URIs
- localhost/127.0.0.1 callbacks are often whitelisted
3. Token Security in Electron Apps
Electron apps use platform-specific secure storage:
- macOS: Keychain via safeStorage API
- Windows: DPAPI
- Linux: libsecret
Cannot extract without reverse-engineering the encryption.
4. Agent Tool Selection
AI agents make autonomous decisions about which tools to use. To force a specific tool:
- Make skill description very explicit
- Add "DO NOT use X" instructions
- Specify binary requirements
- Restart gateway after skill changes
- High priority in metadata
5. Lima VM Best Practices
- Use
mounts: []for complete isolation - Configure port forwards for services that need Mac access
- Use ARM homebrew on Apple Silicon
- Systemd user services for daemons
6. MCP Protocol Basics
# MCP uses JSON-RPC 2.0 over HTTP with SSE
curl -X POST https://mcp.example.com/endpoint \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream, application/json" \
-H "Authorization: Bearer <token>" \
-d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"<tool>","arguments":{}},"id":1}'
Quick Reference
Commands Cheatsheet
# VM Access
/opt/homebrew/bin/limactl shell openclaw
# OpenClaw (run inside VM)
cd ~/openclaw
node openclaw.mjs health # Check status
node openclaw.mjs gateway start # Start gateway
node openclaw.mjs gateway restart # Restart gateway
node openclaw.mjs channels status # Check WhatsApp
node openclaw.mjs skills list # List skills
node openclaw.mjs skills check # Check requirements
node openclaw.mjs logs # View logs
# Swiggy CLI (inside VM)
swiggy-im get_addresses
swiggy-im search_products addressId="<id>" query="<term>"
swiggy-im get_cart
swiggy-im get_orders
File Locations
| File | Purpose |
|---|---|
~/.lima/openclaw/lima.yaml | VM configuration |
~/.openclaw/openclaw.json | OpenClaw config |
~/.openclaw/credentials/swiggy-tokens.json | Swiggy OAuth tokens |
~/.openclaw/workspace/skills/swiggy-instamart/SKILL.md | Skill definition |
~/.local/bin/swiggy-im | Swiggy MCP CLI |
~/.config/systemd/user/openclaw-gateway.service | Gateway service |
Token Refresh
Tokens expire in 5 days. To refresh:
# In VM, run OAuth script again
node /tmp/swiggy-oauth2.js
# Open printed URL in Mac browser
# Complete Swiggy login
# Tokens auto-saved to ~/.openclaw/credentials/swiggy-tokens.json
Conclusion
This integration demonstrates a complete end-to-end flow:
- User sends WhatsApp message → "Search for milk on Instamart"
- OpenClaw Gateway receives message via WhatsApp Web protocol
- Agent (Chotu) processes request using Claude API
- Agent invokes swiggy-instamart skill → runs
swiggy-im search_products - swiggy-im CLI calls Swiggy MCP with Bearer token
- Results returned to agent → formatted response
- Gateway sends reply back to user via WhatsApp
The key breakthrough was understanding that OAuth callbacks must reach the same device running the callback server, solved elegantly with Lima's port forwarding feature.