Operating
Troubleshooting
The errors you will actually see, what each means, and which are worth retrying.
Moove error codes
Every documented code maps to a typed error carrying its retry semantics. The rule, from the API’s own guidance: only 429 and 5xx are worth retrying. Retrying a 4xx cannot succeed, still counts against the rate limit, and a retry loop against a revoked key looks like an attack.
| status · code | typed as | retry | what to do |
|---|---|---|---|
401 UNAUTHENTICATED | MooveAuthError | never | No X-API-Key on a route that needs one. Check the environment. |
401 INVALID_API_KEY | MooveAuthError | never | Unknown, revoked or deactivated. Create a new key. |
401 EXPIRED_API_KEY | MooveAuthError | never | Past its expiry. Create a new key. |
403 INSUFFICIENT_API_SCOPE | MooveScopeError | never | Scopes are fixed at creation. Recreate the key with the Receive agent. |
404 CANNOT_FIND_PAYMENT_LINK | MooveNotFoundError | never | No link with that id. A real answer, not an auth failure — the public read does not fall back on it. |
409 PAYMENT_LINK_ACCOUNT_NOT_READY | MooveAccountNotReadyError | never | Set a handle and a default wallet. See Moove setup. |
422 INVALID_PAYMENT_LINK_AMOUNT | MooveAmountError | never | More decimal places than the settlement token has (six for USDC). |
429 RATE_LIMIT_EXCEEDED | MooveRateLimitError | backoff | No Retry-After is sent. The client backs off with jitter and halves its rate. |
500 CANNOT_CREATE_PAYMENT_LINK | MooveServerError | backoff | Bounded retry, then surface. |
Server and store errors
| symptom | cause | fix |
|---|---|---|
Bad Request: Server not initialized | A transport per request while asking for session ids: the follow-up request lands on a transport that never saw initialize. | Serve MCP statelessly — a fresh server and transport per request, no session id. The reference HTTP entrypoint does this. |
SQLITE_BUSY | Several processes running the schema DDL on one file at once. | Construction retries briefly. If it persists, one process should own the file. |
AmbiguousCommitError | A Postgres COMMIT failed at the connection level. It may have applied. | Deliberately not retried: retrying could spend the same credit twice. Re-read the balance before acting. |
MemoryEntitlementStore holds paid entitlements in memory | The reference store used outside tests. | Use SQLite or Postgres, or acknowledgeEphemeral: true if you really mean it. |
paidTool references sku …, which the provider does not sell | The price is not in the provider's list. | Register the price before the tool. |
Too many requests. Wait Ns | The per-handle rate limit on the reference server. | Separate from the credit balance. It clears on its own. |
[tollbooth] underpaid | A settlement below the 10% floor. Nothing was granted. | Only a person can resolve it. See underpayment. |
The loop does not close
| what you see | likely cause |
|---|---|
| The challenge appears, the user pays, the agent answers from its own knowledge instead of retrying | The one failure mode measured. Copy variant v3 guards against it; an untrustworthy-looking checkout domain makes it worse. |
The agent reports URL elicitation was canceled | A server is emitting the elicitation shape. Tollbooth never does; if you enabled a renderer by allow-list, remove it. |
| The retry is challenged again, forever | Either the payment has not settled, or the charge expired (60 minutes). Check the charge's status; a new challenge opens a new link. |
| A retry says the handle is unknown | Lapsed after 30 days unused, or presented by a different principal than it was bound to. A fresh challenge is issued rather than a hard failure. |