# Spoofy — developer documentation
> Growth for any link — followers, subscribers, members, likes, views, reactions, plays and site visits on ~35 platforms — from code or from an AI agent. Everything the Spoofy web app does is available here.
Three ways in:
- REST API: https://spoofy.social/api/v1 (OpenAPI 3.1 at https://spoofy.social/api/v1/openapi.json)
- SMM-compatible endpoint: https://spoofy.social/api/v2 (key + action)
- MCP server: https://spoofy.social/mcp (Streamable HTTP, stateless)
Money is US dollars as decimal strings ("4.99"). Orders spend real balance. Errors are `{"error": {"code", "message", "details?"}}`.
## Quickstart
```bash
# 1. Find a service (no key needed) — here: X followers
curl "https://spoofy.social/api/v1/services?platform=x"
# 2. Get the exact price for 1,000 (no key needed)
curl -X POST https://spoofy.social/api/v1/quotes \
-H "Content-Type: application/json" \
-d '{"service": 12, "quantity": 1000}'
# 3. Place the order — spends balance. Add "dry_run": true to check without charging.
curl -X POST https://spoofy.social/api/v1/orders \
-H "Authorization: Bearer $SPOOFY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"service": 12, "link": "https://x.com/yourname", "quantity": 1000}'
```
Have a link rather than a platform? `GET /api/v1/links?url=` returns what the link points to and only the services that fit it.
## Authentication and safety rails
Send `Authorization: Bearer fimi_live_…`. Keys are created and revoked by a signed-in person at https://spoofy.social/app/developers; the full key is shown once (we store a hash). An API key can never create keys or manage webhooks. Up to 20 active keys.
- Read-only keys: `scopes: ["read"]` — browse, quote, read orders; never spend, refill or cancel.
- Spend caps: `dailyCap` (per UTC day, net of refunds) and `perOrderCap`, USD strings.
- Platform allow-list: `platforms: ["x", "tiktok"]`.
- Dry runs: `"dry_run": true` on POST /api/v1/orders validates and prices, shows the balance, charges nothing.
- Idempotency: send `Idempotency-Key` on every order; a retry with the same key returns the first order.
- Orders made with a key are labelled with it in the dashboard. No key can withdraw money.
## API reference
Base URL: https://spoofy.social
### Catalog
What can be bought, for which link, at what price. No auth needed.
#### GET /api/v1/services — List services
The full public catalog: every service with its price per 1,000, range and package suggestions.
Auth: no auth.
Parameters:
- `platform` (query): Only one platform, e.g. `x`, `tiktok`, `telegram`.
```bash
curl "https://spoofy.social/api/v1/services?platform=x"
```
Response 200 (The catalog.):
```json
{
"services": [
{
"id": 12,
"platform": "x",
"platformLabel": "X",
"target": "profile",
"metric": "followers",
"metricLabel": "Followers",
"tier": "standard",
"name": "X Followers — Standard",
"description": "Real-looking accounts, steady delivery, 30-day refill.",
"linkHint": "https://x.com/yourname",
"minQty": 100,
"maxQty": 50000,
"pricePer1k": "4.99",
"pricePer1kMicros": "4990000",
"refillDays": 30,
"refillable": true,
"cancelable": true,
"startTime": "0–2 h",
"speed": "2K / day",
"tags": [
"refill"
],
"startEstimate": "Usually starts within 1 h",
"delivery": {
"startMinutes": 60,
"perDay": 2000,
"avgMinutesPer1k": null
},
"packages": [
{
"quantity": 100,
"price": "0.50",
"eta": "~3 h",
"etaMinutes": 132
},
{
"quantity": 500,
"price": "2.50",
"eta": "~7 h",
"etaMinutes": 420
},
{
"quantity": 1000,
"price": "4.99",
"eta": "~13 h",
"etaMinutes": 780
},
{
"quantity": 5000,
"price": "24.95",
"eta": "~3 days",
"etaMinutes": 3660
},
{
"quantity": 50000,
"price": "249.50",
"eta": "~25 days",
"etaMinutes": 36060
}
]
}
]
}
```
Errors: 500 internal.
#### GET /api/v1/services/{id} — Get a service
One service by id.
Auth: no auth.
Parameters:
- `id` (path, required): Service id.
```bash
curl "https://spoofy.social/api/v1/services/12"
```
Response 200 (The service.):
```json
{
"service": {
"id": 12,
"platform": "x",
"platformLabel": "X",
"target": "profile",
"metric": "followers",
"metricLabel": "Followers",
"tier": "standard",
"name": "X Followers — Standard",
"description": "Real-looking accounts, steady delivery, 30-day refill.",
"linkHint": "https://x.com/yourname",
"minQty": 100,
"maxQty": 50000,
"pricePer1k": "4.99",
"pricePer1kMicros": "4990000",
"refillDays": 30,
"refillable": true,
"cancelable": true,
"startTime": "0–2 h",
"speed": "2K / day",
"tags": [
"refill"
],
"startEstimate": "Usually starts within 1 h",
"delivery": {
"startMinutes": 60,
"perDay": 2000,
"avgMinutesPer1k": null
},
"packages": [
{
"quantity": 100,
"price": "0.50",
"eta": "~3 h",
"etaMinutes": 132
},
{
"quantity": 500,
"price": "2.50",
"eta": "~7 h",
"etaMinutes": 420
},
{
"quantity": 1000,
"price": "4.99",
"eta": "~13 h",
"etaMinutes": 780
},
{
"quantity": 5000,
"price": "24.95",
"eta": "~3 days",
"etaMinutes": 3660
},
{
"quantity": 50000,
"price": "249.50",
"eta": "~25 days",
"etaMinutes": 36060
}
]
}
}
```
Errors: 404 not_found.
#### GET /api/v1/links — Inspect a link
What a link points to (platform, profile/post/video/…) and every service that can grow it. Start here when you have a link.
Auth: no auth.
Parameters:
- `url` (query, required): Any link, e.g. https://x.com/yourname.
```bash
curl "https://spoofy.social/api/v1/links?url=https://x.com/yourname"
```
Response 200 (The link and the services that fit it.):
```json
{
"link": {
"url": "https://x.com/yourname",
"host": "x.com",
"platform": "x",
"target": "profile",
"label": "@yourname"
},
"services": [
{
"id": 12,
"platform": "x",
"platformLabel": "X",
"target": "profile",
"metric": "followers",
"metricLabel": "Followers",
"tier": "standard",
"name": "X Followers — Standard",
"description": "Real-looking accounts, steady delivery, 30-day refill.",
"linkHint": "https://x.com/yourname",
"minQty": 100,
"maxQty": 50000,
"pricePer1k": "4.99",
"pricePer1kMicros": "4990000",
"refillDays": 30,
"refillable": true,
"cancelable": true,
"startTime": "0–2 h",
"speed": "2K / day",
"tags": [
"refill"
],
"startEstimate": "Usually starts within 1 h",
"delivery": {
"startMinutes": 60,
"perDay": 2000,
"avgMinutesPer1k": null
},
"packages": [
{
"quantity": 100,
"price": "0.50",
"eta": "~3 h",
"etaMinutes": 132
},
{
"quantity": 500,
"price": "2.50",
"eta": "~7 h",
"etaMinutes": 420
},
{
"quantity": 1000,
"price": "4.99",
"eta": "~13 h",
"etaMinutes": 780
},
{
"quantity": 5000,
"price": "24.95",
"eta": "~3 days",
"etaMinutes": 3660
},
{
"quantity": 50000,
"price": "249.50",
"eta": "~25 days",
"etaMinutes": 36060
}
]
}
]
}
```
Errors: 400 bad_link.
#### POST /api/v1/quotes — Get a quote
The exact price of a quantity. Nothing is charged and no auth is needed.
Auth: no auth.
```bash
curl -X POST "https://spoofy.social/api/v1/quotes" \
-H "Content-Type: application/json" \
-d '{"service":12,"quantity":1000}'
```
Response 200 (The price.):
```json
{
"quote": {
"service": 12,
"quantity": 1000,
"pricePer1k": "4.99",
"total": "4.99",
"currency": "USD"
}
}
```
Errors: 400 bad_quantity, 404 service_unavailable.
### Orders
Place, read, refill and cancel orders. Spends balance.
#### GET /api/v1/orders — List orders
Your orders, newest first. Page with `before` = the smallest order number you have.
Auth: API key (or the web session).
Parameters:
- `status` (query): Filter by status.
- `limit` (query): 1–200, default 50.
- `before` (query): Only orders with a smaller number.
```bash
curl "https://spoofy.social/api/v1/orders" \
-H "Authorization: Bearer $SPOOFY_API_KEY"
```
Response 200 (Orders.):
```json
{
"orders": [
{
"id": 1042,
"service": {
"id": 12,
"name": "X Followers — Standard",
"platform": "x",
"metric": "followers"
},
"link": "https://x.com/yourname",
"quantity": 1000,
"charge": "4.99",
"refunded": "0.00",
"status": "processing",
"startCount": 1520,
"delivered": 300,
"remains": 700,
"refillable": false,
"cancelable": true,
"source": "api",
"createdAt": "2026-10-16T12:00:00.000Z",
"finishedAt": null,
"expectedAt": "2026-10-17T01:00:00.000Z"
}
]
}
```
Errors: 401 invalid_api_key.
#### POST /api/v1/orders — Place an order
Charges your balance and starts delivery. Send an `Idempotency-Key` header so a retry never double-charges. With `dry_run: true` nothing is charged: you get the price, your balance and whether it is enough.
Auth: API key (or the web session).
Parameters:
- `Idempotency-Key` (header): Any unique string (≤ 120 chars) per intended order. Retrying with the same key returns the first order instead of charging twice.
```bash
curl -X POST "https://spoofy.social/api/v1/orders" \
-H "Authorization: Bearer $SPOOFY_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"service":12,"link":"https://x.com/yourname","quantity":1000}'
```
Response 201 (Placed and charged.):
```json
{
"order": {
"id": 1042,
"service": {
"id": 12,
"name": "X Followers — Standard",
"platform": "x",
"metric": "followers"
},
"link": "https://x.com/yourname",
"quantity": 1000,
"charge": "4.99",
"refunded": "0.00",
"status": "pending",
"startCount": null,
"delivered": null,
"remains": null,
"refillable": false,
"cancelable": true,
"source": "api",
"createdAt": "2026-10-16T12:00:00.000Z",
"finishedAt": null,
"expectedAt": "2026-10-17T01:00:00.000Z"
},
"replayed": false,
"charged": "4.99"
}
```
Response 200 (Dry run answer — or a replay of an earlier order with the same Idempotency-Key (`replayed: true`).):
```json
{
"dry_run": true,
"quote": {
"service": 12,
"quantity": 1000,
"total": "4.99",
"currency": "USD"
},
"balance": "12.00",
"sufficient": true
}
```
Errors: 400 invalid_body, 401 invalid_api_key, 402 insufficient_balance, 403 over_order_cap.
#### GET /api/v1/orders/{id} — Get an order
Status and progress of one order.
Auth: API key (or the web session).
Parameters:
- `id` (path, required): Order number.
```bash
curl "https://spoofy.social/api/v1/orders/1042" \
-H "Authorization: Bearer $SPOOFY_API_KEY"
```
Response 200 (The order.):
```json
{
"order": {
"id": 1042,
"service": {
"id": 12,
"name": "X Followers — Standard",
"platform": "x",
"metric": "followers"
},
"link": "https://x.com/yourname",
"quantity": 1000,
"charge": "4.99",
"refunded": "0.00",
"status": "processing",
"startCount": 1520,
"delivered": 300,
"remains": 700,
"refillable": false,
"cancelable": true,
"source": "api",
"createdAt": "2026-10-16T12:00:00.000Z",
"finishedAt": null,
"expectedAt": "2026-10-17T01:00:00.000Z"
}
}
```
Errors: 404 not_found.
#### POST /api/v1/orders/{id}/refill — Request a refill
For a completed order of a refillable service whose count dropped. Carried out in the background.
Auth: API key (or the web session).
Parameters:
- `id` (path, required): Order number.
```bash
curl -X POST "https://spoofy.social/api/v1/orders/1042/refill" \
-H "Authorization: Bearer $SPOOFY_API_KEY"
```
Response 202 (Requested.):
```json
{
"ok": true,
"order": 1042,
"action": "refill",
"status": "requested"
}
```
Errors: 400 not_refillable, 404 not_found.
#### POST /api/v1/orders/{id}/cancel — Request a cancel
For a pending or processing order of a cancelable service. Whatever was not delivered is refunded to your balance.
Auth: API key (or the web session).
Parameters:
- `id` (path, required): Order number.
```bash
curl -X POST "https://spoofy.social/api/v1/orders/1042/cancel" \
-H "Authorization: Bearer $SPOOFY_API_KEY"
```
Response 202 (Requested.):
```json
{
"ok": true,
"order": 1042,
"action": "cancel",
"status": "requested"
}
```
Errors: 400 not_cancelable, 404 not_found.
#### POST /api/v1/uploads — Upload an image
An image for a done-for-you order (logo, banner): multipart field `file`, or the raw bytes with an image Content-Type. PNG, JPG, WEBP or GIF up to 4 MB. Send the returned `id` in the order’s `details`; an upload no order uses is deleted after a day.
Auth: API key (or the web session).
```bash
curl -X POST "https://spoofy.social/api/v1/uploads" \
-H "Authorization: Bearer $SPOOFY_API_KEY"
```
Response 201 (Stored.):
```json
{
"upload": {
"id": "cm2uploadlogo0000000000000",
"mime": "image/png",
"size": 48213,
"width": 512,
"height": 512
}
}
```
Errors: 400 invalid_body, 401 invalid_api_key, 403 over_order_cap.
### Account
Balance.
#### GET /api/v1/balance — Get balance
Your spendable balance in USD.
Auth: API key (or the web session).
```bash
curl "https://spoofy.social/api/v1/balance" \
-H "Authorization: Bearer $SPOOFY_API_KEY"
```
Response 200 (Balance.):
```json
{
"balance": "12.00",
"currency": "USD"
}
```
Errors: 401 invalid_api_key.
### Deposits
Top up the balance with USDG (Robinhood Chain) or the project token (10 % off).
#### GET /api/v1/deposits/methods — Deposit methods
How to top up: your own deposit address per asset (USDG on Robinhood Chain; the project token at 10 % off once live).
Auth: API key (or the web session).
```bash
curl "https://spoofy.social/api/v1/deposits/methods" \
-H "Authorization: Bearer $SPOOFY_API_KEY"
```
Response 200 (Methods.):
```json
{
"methods": [
{
"id": "usdg",
"asset": "USDG",
"chain": {
"id": 4663,
"name": "Robinhood Chain",
"explorer": "https://rh-scan.com"
},
"tokenAddress": "0x0000000000000000000000000000000000000001",
"decimals": 6,
"address": "0x1111111111111111111111111111111111111111",
"treasury": "0x2222222222222222222222222222222222222222",
"minimum": "1.00",
"discountPct": 0,
"priceUsd": null,
"note": "Send any amount of USDG on Robinhood Chain to your address; it is credited 1 USDG = $1."
}
]
}
```
Errors: 401 invalid_api_key.
#### GET /api/v1/deposits — List deposits
Your deposits, newest first.
Auth: API key (or the web session).
```bash
curl "https://spoofy.social/api/v1/deposits" \
-H "Authorization: Bearer $SPOOFY_API_KEY"
```
Response 200 (Deposits.):
```json
{
"deposits": [
{
"id": "cmkd3p0s1t0000qrs",
"asset": "USDG",
"amount": "25.00",
"value": "25.00",
"credit": "25.00",
"txHash": "0xabababababababababababababababababababababababababababababababab",
"status": "credited",
"createdAt": "2026-10-16T11:58:00.000Z",
"creditedAt": "2026-10-16T11:58:20.000Z"
}
]
}
```
Errors: 401 invalid_api_key.
#### POST /api/v1/deposits/claim — Claim a wallet payment
After paying the treasury from a wallet, send the transaction hash: it is verified on chain and credited.
Auth: API key (or the web session).
```bash
curl -X POST "https://spoofy.social/api/v1/deposits/claim" \
-H "Authorization: Bearer $SPOOFY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"txHash":"0xabababababababababababababababababababababababababababababababab"}'
```
Response 200 (Credited.):
```json
{
"deposit": {
"id": "cmkd3p0s1t0000qrs",
"asset": "USDG",
"amount": "25.00",
"value": "25.00",
"credit": "25.00",
"txHash": "0xabababababababababababababababababababababababababababababababab",
"status": "credited",
"createdAt": "2026-10-16T11:58:00.000Z",
"creditedAt": "2026-10-16T11:58:20.000Z"
}
}
```
Errors: 400 invalid_body, 401 invalid_api_key.
### API keys
Create and revoke keys. Signed-in dashboard session only — a key cannot mint keys.
#### GET /api/v1/keys — List API keys
Every key of the signed-in user, revoked ones included. Secrets are never shown again.
Auth: signed-in dashboard session only (an API key is refused).
Response 200 (Keys.):
```json
{
"keys": [
{
"id": "cmk1a2b3c0000xyz",
"name": "Growth agent",
"prefix": "fimi_live_Ab3x",
"scopes": [
"read",
"write"
],
"dailyCap": "50.00",
"perOrderCap": "10.00",
"platforms": [
"x",
"tiktok"
],
"lastUsedAt": null,
"createdAt": "2026-10-16T12:00:00.000Z",
"revokedAt": null
}
]
}
```
Errors: 401 invalid_api_key, 403 session_required.
#### POST /api/v1/keys — Create an API key
Every field is optional. `scopes: ["read"]` makes a read-only key. Caps are USD strings; `platforms` limits which platforms the key may order for. The full key is in this answer only — store it now. Up to 20 active keys.
Auth: signed-in dashboard session only (an API key is refused).
Request body:
```json
{
"name": "Growth agent",
"scopes": [
"read",
"write"
],
"dailyCap": "50.00",
"perOrderCap": "10.00",
"platforms": [
"x",
"tiktok"
]
}
```
Response 201 (Created. `key` is shown once.):
```json
{
"key": "fimi_live_Ab3x…(48 chars)",
"apiKey": {
"id": "cmk1a2b3c0000xyz",
"name": "Growth agent",
"prefix": "fimi_live_Ab3x",
"scopes": [
"read",
"write"
],
"dailyCap": "50.00",
"perOrderCap": "10.00",
"platforms": [
"x",
"tiktok"
],
"lastUsedAt": null,
"createdAt": "2026-10-16T12:00:00.000Z",
"revokedAt": null
}
}
```
Errors: 400 invalid_body, 401 invalid_api_key, 409 too_many_keys.
#### DELETE /api/v1/keys/{id} — Revoke an API key
The key stops working at once.
Auth: signed-in dashboard session only (an API key is refused).
Parameters:
- `id` (path, required): Key id.
Response 200 (Revoked.):
```json
{
"ok": true
}
```
Errors: 404 not_found.
### Webhooks
Signed event delivery to your endpoint. Session only.
#### GET /api/v1/webhooks — List webhooks
Your active webhook endpoints.
Auth: signed-in dashboard session only (an API key is refused).
Response 200 (Webhooks.):
```json
{
"webhooks": [
{
"id": "cmk9w8v7u0000abc",
"url": "https://example.com/spoofy-webhook",
"events": [
"order.updated"
],
"active": true,
"createdAt": "2026-10-16T12:00:00.000Z"
}
]
}
```
Errors: 401 invalid_api_key.
#### POST /api/v1/webhooks — Add a webhook
Public https URL; no `events` means every event. The signing `secret` is in this answer only. Up to 10 webhooks.
Auth: signed-in dashboard session only (an API key is refused).
Request body:
```json
{
"url": "https://example.com/spoofy-webhook",
"events": [
"order.updated"
]
}
```
Response 201 (Created.):
```json
{
"webhook": {
"id": "cmk9w8v7u0000abc",
"url": "https://example.com/spoofy-webhook",
"events": [
"order.updated"
],
"active": true,
"createdAt": "2026-10-16T12:00:00.000Z",
"secret": "whsec_…(shown once)"
}
}
```
Errors: 400 bad_webhook_url, 409 too_many_keys.
#### DELETE /api/v1/webhooks/{id} — Remove a webhook
Stops delivery to this endpoint; queued deliveries are dropped.
Auth: signed-in dashboard session only (an API key is refused).
Parameters:
- `id` (path, required): Webhook id.
Response 200 (Removed.):
```json
{
"ok": true
}
```
Errors: 404 not_found.
### Meta
This document.
#### GET /api/v1/openapi.json — This document
The OpenAPI 3.1 description of the API, for code generators and agents.
Auth: no auth.
```bash
curl "https://spoofy.social/api/v1/openapi.json"
```
## Errors
| HTTP | code | meaning |
|---|---|---|
| 400 | `invalid_body` | The JSON body is missing or has the wrong shape. The message says what to send. |
| 400 | `bad_link` | The link is not a URL we can read. Send the full address, e.g. https://x.com/yourname. |
| 400 | `wrong_platform` | The service is for another platform than the link (e.g. a TikTok service with an X link). |
| 400 | `bad_quantity` | Quantity outside the service range. `details.min` / `details.max` hold the range. |
| 400 | `not_refillable` | This order cannot be refilled (service has no refill, or the order is not completed). |
| 400 | `not_cancelable` | This order cannot be cancelled now. |
| 400 | `chain_not_supported` | The token is on a chain this service does not serve yet (GMGN live viewers: Robinhood Chain only). |
| 400 | `details_required` | A done-for-you service needs a field of its `form`. `details.field` names it. |
| 400 | `bad_details` | A `details` answer is not right (not a link, too long, an unknown or unsuitable upload). `details.field` names it. |
| 400 | `no_file` | The upload carried no file. |
| 400 | `bad_webhook_url` | Webhook URL must be public https://. |
| 401 | `unauthorized` | No API key and no session. Send Authorization: Bearer fimi_live_…. |
| 401 | `invalid_api_key` | The API key is wrong or revoked. |
| 402 | `insufficient_balance` | Balance too low. `details.shortfall` is the USD amount to top up. |
| 403 | `read_only_key` | This key has the read scope only and cannot place, refill or cancel orders. |
| 403 | `platform_not_allowed` | The service's platform is not on this key's allow-list. |
| 403 | `over_order_cap` | The order costs more than this key's per-order cap. |
| 403 | `over_daily_cap` | The order would take this key over its daily spend cap (UTC day, net of refunds). |
| 403 | `session_required` | Keys and webhooks are managed from the signed-in dashboard; an API key cannot manage them. |
| 403 | `bad_origin` | A browser request from another site was refused. |
| 404 | `not_found` | No such order / key / webhook on your account. |
| 404 | `service_unavailable` | The service id does not exist or is paused right now. |
| 409 | `too_many_keys` | You already have 20 active keys. Revoke one first. |
| 409 | `too_many_webhooks` | You already have 10 webhooks. Remove one first. |
| 409 | `link_busy` | As many as this service allows are already running on that link (GMGN: 120 live viewers per token). `details.available` is how many more fit now. |
| 413 | `too_large` | The upload is over 4 MB. |
| 415 | `not_an_image` | The upload is not a PNG, JPG, WEBP or GIF image. |
| 429 | `too_many_uploads` | Too many uploads in the last hour. |
| 500 | `internal` | Something failed on our side. Safe to retry; writes with the same Idempotency-Key never double-charge. |
## Webhooks
Add an https endpoint in the dashboard; events `order.updated`, `deposit.credited`, `balance.low` (none picked = all). Each delivery is a POST of JSON `{ id, type, created, data }` with headers:
- `Fimi-Signature: t=,v1=` — HMAC-SHA256 of `"."` with the `whsec_…` secret.
- `Fimi-Event-Id` — the event id; delivery is at-least-once, drop duplicates.
Answer 2xx within 10 s; otherwise retried with exponential backoff (1 min, 2, 4 … up to 6 h apart) for about 24 h.
```js
import crypto from 'node:crypto';
// header = the Fimi-Signature request header; rawBody = the body exactly as received (a string, not re-serialised)
export function verifySignature(header, rawBody, secret, toleranceSeconds = 300) {
if (!header) return false;
let t = null;
const sigs = [];
for (const part of header.split(',')) {
const [k, v] = part.split('=');
if (k === 't') t = Number(v);
if (k === 'v1') sigs.push(v);
}
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false; // stale: possible replay
const want = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return sigs.some((s) => s.length === want.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(want)));
}
// Express: keep the raw body for this route
app.post('/spoofy-webhook', express.text({ type: '*/*' }), (req, res) => {
if (!verifySignature(req.get('Fimi-Signature'), req.body, process.env.SPOOFY_WEBHOOK_SECRET)) return res.sendStatus(400);
const event = JSON.parse(req.body); // { id, type, created, data }
// drop duplicates by event.id (also in the Fimi-Event-Id header): deliveries are at-least-once
res.sendStatus(200);
});
```
Example `order.updated` body:
```json
{
"id": "evt_cmk5e6f7g0000hij",
"type": "order.updated",
"created": 1792152000,
"data": {
"order": {
"id": 1042,
"service": {
"id": 12,
"name": "X Followers — Standard",
"platform": "x",
"metric": "followers"
},
"link": "https://x.com/yourname",
"quantity": 1000,
"charge": "4.99",
"refunded": "0.00",
"status": "processing",
"startCount": 1520,
"delivered": 300,
"remains": 700,
"refillable": false,
"cancelable": true,
"source": "api",
"createdAt": "2026-10-16T12:00:00.000Z",
"finishedAt": null,
"expectedAt": "2026-10-17T01:00:00.000Z"
}
}
}
```
## SMM-compatible endpoint
URL https://spoofy.social/api/v2; the same Spoofy API key, caps and prices. POST form-encoded (JSON and query string also work). Errors are `{"error": "…"}` with HTTP 200.
| action | params | answer |
|---|---|---|
| services | | `[{service, name, type, category, rate, min, max, refill, cancel}]` — rate = USD per 1,000 |
| add | service, link, quantity | `{order}` |
| status | order | `{charge, start_count, status, remains, currency}` |
| status | orders=1,2,3 (≤100) | `{"1": {…}, "2": {"error": …}}` |
| refill | order or orders | `{refill}` or `[{order, refill}]` |
| cancel | order or orders | `{order, cancel}` or `[{order, cancel}]` |
| balance | | `{balance, currency: "USD"}` |
Status words: Pending, Processing, In progress, Completed, Partial, Canceled. `charge` is net of refunds.
```bash
curl -X POST https://spoofy.social/api/v2 -d key=$SPOOFY_API_KEY -d action=services
curl -X POST https://spoofy.social/api/v2 -d key=$SPOOFY_API_KEY -d action=add -d service=12 -d link=https://x.com/yourname -d quantity=1000
curl -X POST https://spoofy.social/api/v2 -d key=$SPOOFY_API_KEY -d action=status -d order=1042
curl -X POST https://spoofy.social/api/v2 -d key=$SPOOFY_API_KEY -d action=status -d orders=1042,1043
curl -X POST https://spoofy.social/api/v2 -d key=$SPOOFY_API_KEY -d action=balance
```
## AI agents (MCP)
Remote MCP server at https://spoofy.social/mcp, Streamable HTTP, stateless, auth `Authorization: Bearer fimi_live_…`. Give an agent a key with a daily cap and a per-order cap.
### Claude Code
Run in a terminal:
```bash
claude mcp add --transport http spoofy https://spoofy.social/mcp --header "Authorization: Bearer YOUR_KEY"
```
### Cursor
Add to .cursor/mcp.json in your project (or ~/.cursor/mcp.json for every project):
```json
{
"mcpServers": {
"spoofy": {
"url": "https://spoofy.social/mcp",
"headers": {
"Authorization": "Bearer YOUR_KEY"
}
}
}
}
```
### Claude Desktop
Settings → Developer → Edit Config (claude_desktop_config.json), then restart Claude. Needs Node.js:
```json
{
"mcpServers": {
"spoofy": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://spoofy.social/mcp",
"--header",
"Authorization:${SPOOFY_AUTH}"
],
"env": {
"SPOOFY_AUTH": "Bearer YOUR_KEY"
}
}
}
}
```
### VS Code (Copilot agent mode)
Add to .vscode/mcp.json:
```json
{
"servers": {
"spoofy": {
"type": "http",
"url": "https://spoofy.social/mcp",
"headers": {
"Authorization": "Bearer YOUR_KEY"
}
}
}
}
```
### Any MCP client
Streamable HTTP transport, stateless (no session needed):
```bash
URL: https://spoofy.social/mcp
Header: Authorization: Bearer YOUR_KEY
Method: POST JSON-RPC 2.0 (Accept: application/json, text/event-stream)
```
### Tools
- `find_services`: Find what Spoofy can do for a link or a goal, with prices. ALWAYS call this first: service ids come from here. Give `link` (any profile, post, video, channel, group, token page or website URL) and/or `goal` in plain words, e.g. "500 followers on my tiktok" or "cheap views for this YouTube video". With a link you only get services that fit that exact link (a profile link gets followers, a post link gets likes/views). Returns matching services: `service` (the id to quote/order), name, tier (basic/standard/premium), pricePer1k (USD per 1,000 units), min/max quantity, start time, speed, refill days, preset packages with USD prices, and — when the goal names a quantity — a `suggestion` with the exact total for that quantity. Free; spends nothing.
- `get_quote`: Exact price in USD for a quantity of one service (from find_services). Checks the quantity is within the service range. Free; spends nothing. Use it to tell the user the cost before place_order.
- `place_order` (acts): Place an order. THIS SPENDS THE USER'S REAL BALANCE (USD) and starts delivery to the link; it cannot be undone except by cancel_order where the service allows it. Before calling: get the price with get_quote and get the user's explicit agreement to that price. Then call with `confirm: true`. Without `confirm: true` nothing is charged and you get the price back so you can ask. `dry_run: true` validates everything (link fits the service, quantity range, key limits) and returns price, balance and whether it is enough — never charges. Pass a fresh `idempotency_key` per intended order so a retry never charges twice. Errors: insufficient_balance (details.shortfall = USD to top up — call get_top_up_info), over_order_cap / over_daily_cap / platform_not_allowed / read_only_key (this key's limits), wrong_platform, bad_quantity, link_busy (too many running on that link: details.available), chain_not_supported. Done-for-you services (kind "manual": listings) need a form with images — those are ordered on the website.
- `get_order`: Status and progress of one order: status (pending, processing, completed, partial, refunded, cancelled, failed), start count, delivered, remains, charge and refunds in USD, and whether refill/cancel are possible now.
- `list_orders`: The user's orders, newest first. Filter by status; page with `before` = the smallest order number already seen.
- `request_refill` (acts): Ask for a refill of a completed order whose count dropped (only when the order says refillable: true). Free. Carried out in the background; check progress with get_order.
- `cancel_order` (acts): Ask to cancel a pending or processing order (only when the order says cancelable: true). Whatever was not delivered is refunded to the balance in USD. Confirm with the user first.
- `get_balance`: The user's spendable balance in USD. Orders are paid from it.
- `get_top_up_info`: How the user adds money to the balance: their own deposit address for USDG on Robinhood Chain (credited 1 USDG = $1) and, once live, the project token at 10 % off, plus the top-up page link. Use it when an order fails with insufficient_balance. You cannot move money yourself — give the user the address or the link.
Raw JSON-RPC:
```bash
curl -X POST https://spoofy.social/mcp \
-H "Authorization: Bearer $SPOOFY_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"find_services","arguments":{"goal":"500 followers","link":"https://x.com/yourname"}}}'
```
## TypeScript SDK (@fimi/sdk)
Zero dependencies, fetch-based; Node 18+, Bun, Deno, browsers, edge. `orders.create` sends an auto-generated idempotency key. Methods: services.list/get, links.inspect, quotes.create, orders.create/list/get/refill/cancel, balance.get, deposits.methods/list/claim, webhooks.verify(signatureHeader, rawBody, secret).
```ts
import { Spoofy } from '@fimi/sdk';
const spoofy = new Spoofy({ apiKey: process.env.SPOOFY_API_KEY });
const { link, services } = await spoofy.links.inspect('https://x.com/yourname');
const service = services.find((s) => s.metric === 'followers')!;
const quote = await spoofy.quotes.create({ service: service.id, quantity: 1000 });
const check = await spoofy.orders.create({ service: service.id, link: link.url, quantity: 1000, dry_run: true });
if (check.sufficient) {
const { order } = await spoofy.orders.create({ service: service.id, link: link.url, quantity: 1000 });
console.log(`#${order.id} ${order.status} — charged $${order.charge} (quoted $${quote.total})`);
}
```