A deliberately small example of how an AI agent can use the Atomic API and Transact SDK to help a user find and cancel their subscriptions.
The user chats with an assistant in a mobile app. The agent looks merchants up through the Atomic API. When the user is ready to cancel, the agent puts a button in the app that opens Atomic's Transact SDK, where the user finishes the cancellation. Atomic's webhooks and the SDK's events go back to the agent, so it can tell the user how things went without being asked.
It's meant to be read, not deployed. There's no login, state lives in memory or a local SQLite file, and webhooks aren't verified. Each of those is a place where a real integration would do more.
| Part | What it uses |
|---|---|
| Agent | LangChain createAgent with Claude (claude-opus-5-5) through @langchain/anthropic. Conversation memory is LangGraph's in-memory MemorySaver. |
| Server | Node.js 22+ and Express 5, a WebSocket server (ws), and SQLite through Node's built-in node:sqlite for webhook events. |
| Atomic | The Atomic API, called from the server with your API key and secret, and the Transact SDK, opened in the app with a public token. |
| Mobile app | Expo (SDK 57) / React Native with @atomicfi/transact-react-native. It needs a development build because the Transact SDK has native code, so Expo Go won't run it. |
Everything the agent responds to goes through one queue. Three things write to it: the user, Atomic's webhooks, and the Transact SDK's events. The agent takes items off the queue one at a time for each user and sends each reply to the app over a WebSocket.
- The user's messages go to
POST /user-message, and the server queues them. - Transact SDK events: when the user leaves the Atomic screen, the app sends the SDK's
onFinishoronClosepayload toPOST /transact-events. The server queues it as a system event, so the agent can check on the task and follow up. - Atomic webhooks arrive at
POST /webhooks/atomic/manage-agent.task-status-updatedevents are saved to SQLite, and when a task finishes, the server also queues it. A cancellation is followed by a refresh task that resyncs the user's data; once the refresh completes, the agent reads the account to confirm the cancellation. - The agent sees system events as
<system_event type="…">messages the user never sees. Its system prompt says how to respond to each type. - Replies go to the app over the WebSocket, along with
busy(for the typing indicator) anderrorevents. Each user has one thread, keyed by their user ID.
When the agent wants the app to open Transact, a tool returns a launch_transact client action alongside its result. It travels with the agent's reply, and the app shows it as a button. The model only learns that the button is ready; it never sees the launch configuration.
agent/ The agent: model, system prompt, tools
tools/ One file per tool
atomic/ A small client for the Atomic API
server/
index.js Express app and WebSocket server
queue.js Per-thread message queue; runs the agent's turns
bus.js Sends events to the WebSockets listening on a thread
db.js SQLite: the test user and stored webhook events
routes/ /user-message, /transact-events, /webhooks/atomic/manage-agent
mobile/ Expo app: chat screen, API client, Transact launcher
The agent has seven tools. Each runs on the server, so Atomic credentials never reach the model or the app.
| Tool | What it does | Atomic API |
|---|---|---|
upsert_user |
Makes sure Atomic knows the user, by creating them if needed. Called at the start of each conversation. | POST /access-token |
find_company |
Searches for a company by name and says whether Atomic can cancel subscriptions there. | POST /company/search |
list_cancellable_companies |
Lists every company where Atomic supports cancelling a plan. | GET /company/list?actions=cancel-plan |
get_user_subscriptions |
Shows the user's connected accounts, with the bills and plans Atomic found on each. | GET /pay-link/accounts |
get_actions_for_company |
Gets what Atomic can do for this user at a merchant: cancel-plan for a specific subscription, or connect-account if the user needs to sign in first. |
GET /pay-link/actions-for-company/:companyId |
get_action_config |
Creates a public token and gives the app a "Cancel <company>" or "Connect <company>" button that opens Transact on a cancel-plan or connect-account action. Returns the settings the app needs to launch Transact on the right action. |
POST /access-token |
check_events |
Reads the latest task statuses from the stored webhooks, to tell the user whether a cancellation went through. | None. It reads the local database. |
The project uses Atomic's sandbox by default. .env.example points at https://sandbox-api.atomicfi.com, and the Transact SDK uses the same environment.
- Node.js 22.13 or later, with npm
- macOS with Xcode and an iOS simulator running iOS 16.4 or later, plus CocoaPods, to build the app. It has been built with Xcode 27.0 and CocoaPods 1.16.2. The app is iOS only: there's an
androidscript, but it hasn't been built or tested for Android. It needs a native build because the Transact SDK has native code, so Expo Go won't run it. - Ports 3000 and 8081 free, for the server and Metro. If another dev server is on port 3000, the app's requests reach it instead and fail with a 404. To use a different port, start the server with
PORT=3001 npm run devand setEXPO_PUBLIC_API_URLinmobile/.envto match, then restart Metro withnpx expo start -cso the app picks it up. - An Atomic account, to get sandbox API keys from the Atomic console. If you don't have one, contact Atomic to get set up. Besides the PayLink endpoints, the keys need access to list API secrets and to create and delete webhook endpoints, for the automatic webhook setup.
- An Anthropic account, to get an API key from the Claude Console
- An ngrok account, for the authtoken the server uses to open a tunnel so Atomic's webhooks can reach your machine. A free account works, and you don't need the ngrok CLI.
npm install
cd mobile && npm install && cd ..Copy the example env file and fill in your keys:
cp .env.example .envANTHROPIC_API_KEY=sk-ant-...
ATOMIC_API_BASE_URL=https://sandbox-api.atomicfi.com
ATOMIC_API_KEY=... # sandbox key from the Atomic console
ATOMIC_API_SECRET=... # sandbox secret from the Atomic console
USER_IDENTIFIER=... # required: the user every chat acts as, such as test-user-1
NGROK_AUTHTOKEN=... # from the ngrok dashboard
NGROK_DOMAIN= # optional: a reserved domain on a paid ngrok planAll of these except NGROK_DOMAIN are required. If any is missing, the server stops at startup and lists the ones it needs.
Tell the app where the server is in mobile/.env:
# localhost works in the iOS simulator; on a real device use your Mac's LAN IP.
EXPO_PUBLIC_API_URL=http://localhost:3000npm run devIt listens on http://localhost:3000 and creates data.db with one test user, USER_IDENTIFIER from .env. Every chat acts as that user. It also opens an ngrok tunnel and points Atomic's webhooks at it; see step 5.
In another terminal:
npm run iosThis builds the app, installs it on the simulator and starts Metro. The first build takes a few minutes. The header shows Online once the app's WebSocket connects.
Webhooks are how the agent learns whether a cancellation went through. The Transact SDK's events only say that the user left the Atomic screen, not how the task ended. Without webhooks, the agent can't confirm any outcome.
The server sets them up for you when it starts:
- It opens an ngrok tunnel to itself, using
NGROK_AUTHTOKEN. The tunnel gets a new URL every time, unless you setNGROK_DOMAINto a domain reserved on a paid ngrok plan. - It creates a new endpoint at
<tunnel URL>/webhooks/atomic/manage-agentfortask-status-updated, signed with your API secret nameddefault, or your first secret if none has that name.
When the server stops, with Ctrl+C or when npm run dev restarts it after a change, it deletes the endpoint it created. If it's killed outright, the endpoint stays in Atomic, and you can delete it in the Atomic console.
The log shows the tunnel and the endpoint it registered. If any step fails, the server logs an error starting with [ngrok] or [webhooks]. The app still starts, but the agent won't hear when a cancellation finishes.
To try the flow without Atomic, you can send a webhook yourself while the app is open:
curl -X POST localhost:3000/webhooks/atomic/manage-agent -H 'content-type: application/json' -d '{
"eventType": "task-status-updated", "product": "action", "task": "demo-task-1",
"user": { "identifier": "<your USER_IDENTIFIER>" }, "company": { "name": "Netflix" },
"data": { "status": "completed", "flow": "cancel-plan" }
}'The agent's message should appear in the chat a few seconds later. Repeat deliveries of a finished task are ignored, so use a new task value each time.