Appearance
Running multiple accounts: mother-slave dedup
A single character can run on several accounts of the same platform — for example two Instagram accounts, @ava.main and @ava.vip, both driven by the character "Ava". Because both accounts authenticate with the same per-character connector token, every inbound from either account lands on the same character, the same (platform, handle) lead, and the same cross-surface Person. Left alone, that means both accounts would generate a reply to the same lead — and the lead gets answered twice.
Mother-slave dedup fixes that. With it enabled on the character, the first account a lead messages claims that lead; any later inbound for the same lead from a different account of the same character is ignored, so only one account ever replies. This guide shows how to turn it on, how to tell the API which account received each message, and exactly what an ignored call looks like.
All calls go to the production base URL https://api-talk.fluidvip.com/api/v1/characters and authenticate with your character's connector token (see authentication.md). Dedup touches three endpoints: POST /chat, POST /triggers, and GET /followups.
For the data model behind characters, leads, sessions, and the cross-surface Person, see concepts.md. For the full request/response shapes, see reference/chat.md, reference/triggers.md, and reference/followups.md.
How a claim works
Dedup is decided per lead — a lead is (platform, handle). The first account to engage a lead claims it; thereafter only that account is served for that lead, on that platform.
@mark → DM → @ava.main (first) ⇒ @ava.main CLAIMS "mark" on instagram → character replies
@mark → DM → @ava.vip (later) ⇒ already claimed by @ava.main → { ignored: true }, send nothing
@mark → DM → @ava.main (again) ⇒ the owning account messaging again → character replies normally
@mark → DM → @ava.fan (fanvue) ⇒ DIFFERENT platform → its own claim → character replies (funnel crossover)- The lead is still one Person — Ava stays consistent and remembers
@markno matter which account he reaches. Dedup only decides which account answers, never what the character knows. - The claim is recorded the first time an account engages the lead (a
/chatturn or a/triggersopen). It does not expire on its own — a re-engaged lead stays with the account that first claimed him. - Claims are per platform. A claim on Instagram never blocks a different platform's account of the same character — that is the funnel crossover (IG → Fanvue), and it is preserved by design. See Cross-platform: claims are per platform.
Turn it on
Mother-slave dedup is a per-character setting. Enable it from the character's platform settings in the dashboard at talk.fluidvip.com — the same screen where you get the connector token.
Dedup is off by default. With it off, the API behaves exactly as before: every account is served, and the
own_usernamefield below is ignored. You only need the rest of this guide once you actually run two or more accounts of one character on the same platform.
Once it is on, the API needs to know which of your accounts received each message — otherwise it cannot tell two accounts apart (they share one token). You supply that with own_username.
Tell the API which account received the message
own_username is the bot account's own handle that received this inbound (e.g. ava.main) — not the lead's handle, and not the character's advertised/share username. Pass it on every dedup-relevant call:
| Endpoint | Where | Field |
|---|---|---|
POST /chat | request body | own_username |
POST /triggers | request body | own_username |
GET /followups | query string | own_username |
The handle is normalized the same way the lead handle is (a leading @ is stripped and case is ignored), so @Ava.Main, ava.main, and AVA.MAIN all name the same account.
own_usernamenever participates in authentication or routing — your token still selects the character, andplatformin the body still selects the bound workflow. It is pure claim metadata. Omit it (or send a junk value like""/sim) and the call is treated as account-unaware: it falls back to normal, served behavior.
On POST /chat
Send own_username alongside the inbound DM. If the lead is already claimed by a different account, the call is ignored and you send nothing.
bash
curl -X POST https://api-talk.fluidvip.com/api/v1/characters/chat \
-H "X-Connector-Token: ftc_live_8f3c..." \
-H "Content-Type: application/json" \
-d '{
"platform": "instagram",
"handle": "mark",
"message": "hey ava",
"own_username": "ava.main"
}'python
from fluidtalk import FluidTalk
ft = FluidTalk(token="ftc_live_8f3c...")
reply = ft.chat(
platform="instagram",
handle="mark",
message="hey ava",
own_username="ava.main",
)
if getattr(reply, "ignored", False):
pass # another account already owns this lead — send nothing
else:
for bubble in reply.bubbles:
send_to_instagram(bubble.text, after_ms=bubble.delay_ms)typescript
import { FluidTalk } from "fluidtalk";
const ft = new FluidTalk({ token: "ftc_live_8f3c..." });
const reply = await ft.chat({
platform: "instagram",
handle: "mark",
message: "hey ava",
ownUsername: "ava.main",
});
if (reply.ignored) {
// another account already owns this lead — send nothing
} else {
for (const bubble of reply.bubbles) {
await sendToInstagram(bubble.text, bubble.delayMs);
}
}When a different account hits a claimed lead, the reply carries no bubbles and is flagged as ignored:
json
{
"data": {
"session_id": null,
"bubbles": [],
"ignored": true,
"ignore_reason": "lead_claimed_by_other_account"
},
"request_id": "req_8a1c2d3e4f"
}session_id is null on an ignored call (the API never leaks the owning account's session across accounts). A normal, served /chat reply has its usual shape — session_id set and a non-empty bubbles list — and no ignored field.
On POST /triggers
Conversation triggers (a story reaction, a new follower, a custom event) follow the same rule: a cold-open or warm-advance from a non-owning account is ignored, so a slave account neither opens nor advances a claimed lead.
bash
curl -X POST https://api-talk.fluidvip.com/api/v1/characters/triggers \
-H "X-Connector-Token: ftc_live_8f3c..." \
-H "Content-Type: application/json" \
-d '{
"platform": "instagram",
"handle": "mark",
"event_id": "story_reaction",
"external_event_id": "ig_evt_55812",
"own_username": "ava.main"
}'python
res = ft.trigger(
platform="instagram",
handle="mark",
event_id="story_reaction",
external_event_id="ig_evt_55812",
own_username="ava.main",
)
if getattr(res, "ignored", False):
pass # claimed by another account — send nothing
elif res.opened:
for bubble in res.bubbles:
send_to_instagram(bubble.text, after_ms=bubble.delay_ms)typescript
const res = await ft.trigger({
platform: "instagram",
handle: "mark",
eventId: "story_reaction",
externalEventId: "ig_evt_55812",
ownUsername: "ava.main",
});
if (res.ignored) {
// claimed by another account — send nothing
} else if (res.opened) {
for (const bubble of res.bubbles) {
await sendToInstagram(bubble.text, bubble.delayMs);
}
}An ignored trigger looks like:
json
{
"data": {
"ok": true,
"ignored": true,
"ignore_reason": "lead_claimed_by_other_account",
"bubbles": []
},
"request_id": "req_3f9b0a7c21"
}A cold trigger to a brand-new lead is that lead's first contact, so it legitimately claims him for the account that fired it — exactly as a first
/chatwould.
On GET /followups
Proactive follow-ups are queued for the owning account and delivered to it. Pass own_username as a query parameter and you receive unclaimed follow-ups plus follow-ups claimed by that account — never another account's claimed rekindles. Acknowledge each one as usual.
bash
curl -G https://api-talk.fluidvip.com/api/v1/characters/followups \
-H "X-Connector-Token: ftc_live_8f3c..." \
--data-urlencode "platform=instagram" \
--data-urlencode "own_username=ava.main"python
pending = ft.followups.list(platform="instagram", own_username="ava.main")
for f in pending.followups:
send_to_instagram(f.message, to=f.handle)
ft.followups.ack(f.id)typescript
const pending = await ft.followups.list({
platform: "instagram",
ownUsername: "ava.main",
});
for (const f of pending.followups) {
await sendToInstagram(f.message, f.handle);
await ft.followups.ack(f.id);
}Once dedup is on, always pass
own_usernamewhen pulling follow-ups. Omit it and the account receives only the unclaimed rows — any rekindles already attributed to a specific account are withheld, so claimed-lead follow-ups silently stop going out.
What "first account claims the lead" means
The decision for an inbound on a given (platform, handle) is:
| Situation | Result | You should |
|---|---|---|
| First contact — no account has engaged this lead yet | the calling account claims him, the turn runs | send the returned bubbles |
Same account — own_username matches the claim | served normally | send the returned bubbles |
| Different account — claimed by someone else | ignored: true, empty bubbles, session_id: null | send nothing |
| Different platform — claim is on another platform | not blocked; runs and claims independently | send the returned bubbles |
Dedup off, or no own_username | served normally (account-unaware) | send the returned bubbles |
ignore_reason is the canonical machine-readable field — branch on it (or on ignored) rather than on the empty bubbles list, since a closed conversation also returns empty bubbles but is not an ignore.
Cross-platform: claims are per platform
A claim lives on the lead's per-platform identity, so it only governs the platform it was made on. @mark, claimed on Instagram by @ava.main, does not stop a Fanvue account of the same character from picking him up after an Instagram → Fanvue link drop. That hop is the intended funnel crossover, and it is a separate identity with its own, independent claim.
This is the whole point of keeping dedup per platform: it suppresses duplicate accounts on one platform without ever blocking the multi-platform funnel that moves a lead from a free platform to a paid one. (Cross-platform recognition of the same lead is still the self-claim handshake — see concepts.md.)
Default off, and turning it on mid-flight
When dedup is off, none of the above applies: own_username is ignored, every account is served, and every queued follow-up is delivered to any caller — behavior is identical to a single-account character.
When you flip it on for a character that already had both accounts talking to the same leads, ownership is decided forward-only: the first account to message each lead after you enable it wins the claim. Existing conversations are not disturbed — the lead's session, rapport, and history are kept intact; dedup only starts gating new inbound from the losing account.
Errors you may hit here
| Status | Meaning in this context |
|---|---|
400 | invalid_request — malformed body, e.g. a missing platform or handle, or a non-string own_username. |
401 | invalid_token — missing, wrong, or revoked X-Connector-Token. |
402 | payment_required — the served account's turn couldn't be billed; an ignored (slave) call is never charged. See Billing. |
403 | forbidden — the token can't act on this platform/character. |
422 | validation_error — e.g. GET /followups without the required platform. |
429 | rate_limited — honor Retry-After and retry. |
Every response carries the envelope { "data": … } on success or { "error": { "code", "message", "type", "request_id" } } on failure, plus an X-Request-Id header for support. See errors.md for the full model and rate-limits.md for per-token limits. The SDKs raise typed errors (AuthError for 401, PaymentRequiredError for 402, ValidationError for 422, and so on).
Related
- Sending and receiving DMs — the
/chatturn and the bubble list dedup wraps. - Triggers and entry points — fire story reactions, new-follower, and custom events with
own_username. - Follow-ups — pull, deliver, and ack proactive rekindles per account.
- Billing — metered usage; ignored (slave) calls are never charged.
- Reference: chat · triggers · followups
- SDKs: Python · TypeScript