Swiggy Instamart + OpenClaw Integration via WhatsApp

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_addressesGet saved delivery addresses
search_productsSearch products by query
your_go_to_itemsFrequently ordered items
get_cartView current cart
update_cartAdd/update cart items
clear_cartEmpty the cart
checkoutPlace order (COD, max ₹999)
get_ordersOrder history
track_orderReal-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

  1. Added Swiggy to Cursor's MCP config (~/.cursor/mcp.json)
  2. Completed OAuth successfully in Cursor
  3. Problem: Tokens stored encrypted in macOS Keychain via Electron's safeStorage API
  4. 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

  1. Run script in VM: node /tmp/swiggy-oauth2.js
  2. Copy OAuth URL printed in terminal
  3. Open URL in Mac browser (not VM)
  4. Complete Swiggy login
  5. Browser redirects to localhost:8080/callback
  6. Port forwarding sends callback to VM
  7. 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:

  1. A custom CLI script (swiggy-im) that calls Swiggy MCP directly with curl
  2. 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:

Solutions tried:

  1. Updated skill description to say "DO NOT use browser"
  2. Added requires.bins: ["swiggy-im"] to skill metadata
  3. Added priority: high to skill metadata
  4. 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 isolationmounts: [] keeps VM completely separate from Mac
Port forwardingEnables OAuth callback from Mac browser to VM
Custom OAuth scriptPKCE flow with manual browser step
Direct MCP callsUsing fetch() with Bearer token
Custom CLI wrapperswiggy-im script abstracts MCP protocol
OpenClaw skill systemSKILL.md provides instructions to agent
WhatsApp integrationWorks seamlessly once skill is recognized

What Didn't Work

Approach Why It Failed
mcporter authSwiggy doesn't recognize mcporter's OAuth redirect
Cursor token extractionEncrypted with Electron safeStorage + Keychain
npm link in VMPermission issues with global node_modules
X11 forwardingWould work but more complex than port forwarding
Browser in VMHeadless 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:

3. Token Security in Electron Apps

Electron apps use platform-specific secure storage:

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:

5. Lima VM Best Practices

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.yamlVM configuration
~/.openclaw/openclaw.jsonOpenClaw config
~/.openclaw/credentials/swiggy-tokens.jsonSwiggy OAuth tokens
~/.openclaw/workspace/skills/swiggy-instamart/SKILL.mdSkill definition
~/.local/bin/swiggy-imSwiggy MCP CLI
~/.config/systemd/user/openclaw-gateway.serviceGateway 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:

  1. User sends WhatsApp message → "Search for milk on Instamart"
  2. OpenClaw Gateway receives message via WhatsApp Web protocol
  3. Agent (Chotu) processes request using Claude API
  4. Agent invokes swiggy-instamart skill → runs swiggy-im search_products
  5. swiggy-im CLI calls Swiggy MCP with Bearer token
  6. Results returned to agent → formatted response
  7. 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.