Answers
If my agent retries a tool call after a timeout, will I be charged twice?
Short answer
Not if the retry carries the same idempotency key as the first attempt: okdata keeps each successful result for 24 hours and hands it back with no new charge, so a retried X search costs $0.006 once. Failed calls are never charged, and after a timeout you resend with the same key to find out whether the first attempt went through.
Here is how it usually goes wrong. The agent asks for page 3 of an X search, the client gives up after 60 seconds, the server finishes the call anyway, and the agent asks again. Without protection you pay for page 3 twice and the agent may process the same posts twice. The MCP spec itself is still working on this: an open issue shows a lost response leading to a re-executed tool call, and a proposal for request idempotency is under discussion. Until clients handle it, the fix lives in the API you call.
How retries work on okdata
Send a random key with each logical request and reuse it on every retry of that request:
- HTTP: an
Idempotency-Keyheader. - MCP: the
idempotency_keyargument of thecalltool.
The rules, from the errors page:
- Same key and same request within 24 hours: you get the stored result with
meta.replayed: true,meta.billing.amount: "0", the originalrequest_idand anIdempotent-Replayed: trueheader. No new charge. - Same key, different request: rejected as
invalid_params, so a reused key can never return the wrong data. - Failed calls are not stored. Retrying a failure with the same key simply runs it again, and a failure was free anyway.
- A retry that arrives while the first attempt is still running gets
rate_limitedwithretry_after_seconds: 1. Wait a second and send it again with the same key.
What to do with each outcome
| Response | Charged | Next step |
|---|---|---|
ok |
yes, once | use it |
timeout (504) |
not known yet (charged: null) |
resend with the same key, or look it up at /v1/calls/{request_id} |
upstream_busy, upstream_failed, internal_error |
no | retry with the same key |
rate_limited (429) |
no | wait retry_after_seconds, retry with the same key |
invalid_params, insufficient_balance |
no | fix the request or top up first |
not_found |
no | do not retry |
What the key saves
| Job: 5 pages of X search | Calls billed | Cost |
|---|---|---|
| Each page retried 3 times, no key | 15 | $0.09 |
| Same retries with a key | 5 | $0.03 |
The same holds for Douyin video search at $0.02 a page, or any other endpoint such as X post search; the more expensive the endpoint, the more a missing key costs.
Code
import os, time, uuid
import requests
URL = "https://api.okdata.online/v1/x/posts/search"
H = {"Authorization": f"Bearer {os.environ['OKDATA_API_KEY']}"}
def search_page(query, cursor=None):
key = str(uuid.uuid4()) # one key for this page, reused below
params = {"query": query, "sort": "latest", **({"cursor": cursor} if cursor else {})}
for attempt in range(4):
try:
body = requests.get(URL, headers={**H, "Idempotency-Key": key},
params=params, timeout=60).json()
except requests.Timeout:
continue # same key: a finished call replays for free
if body["code"] == "ok":
return body
if body["retry"] in ("yes", "same_key"):
time.sleep(body.get("retry_after_seconds") or 2 ** attempt)
continue
raise RuntimeError(f"{body['code']}: {body['message']}")
raise RuntimeError("no answer after 4 tries")
If your agent talks MCP, tell it in the system prompt: "When you retry call, pass the same idempotency_key as the first attempt."
Alternatives
A dedupe cache in your own code (store results by request hash) prevents double processing inside one process, but it cannot stop a second charge if the first response never arrived. Some APIs offer no idempotency at all; with those, keep timeouts generous and avoid automatic retries on calls that cost money. Request shapes and headers are on the requests page.
Updated