mcpbeat Sign in

API Ok But Ignored Agent Skill

A remote write can answer "ok" and still have discarded what you sent, saying so only in a secondary field riding along with the success. Model accepted-but-discarded as its own outcome, read that field on every write, and log a discard loudly. Reach for it when a submission reports success on every call and the data never appears on the other side.

2k tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
100
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/maxrave-dev/kotlin-footguns --skill api-ok-but-ignored

The instruction itself

3 sections, as written by the author

"ok" is a transport answer, not a semantic one

A write to a remote service has two independent verdicts, and most clients only read the first:

  • Did the request arrive and parse? — the HTTP status, the top-level ok/error field.
  • Did the service keep what you sent? — reported separately, or not at all.

Services that accept high write volumes routinely answer the first question with success while

answering the second with silence: the record is filtered, deduplicated, clamped, or dropped, and

the only trace is a small object nested inside the success body. A client that branches on the

status alone reports a perfect success rate while the other side stays empty.

Model the two verdicts as three outcomes, not as a boolean:

// adapted — names generalized
sealed interface WriteOutcome {
    data object Ok : WriteOutcome
    data class Ignored(val code: Int, val message: String) : WriteOutcome  // accepted, then discarded
    data class Error(val code: Int, val message: String) : WriteOutcome    // never accepted
}

Three states means a caller physically cannot collapse "kept" and "discarded" into one branch: the

when does not compile until it handles both.

Traps

The discard reason is nested under the item, not at the top level. It rides *inside* the

per-record part of the success body, so a reader that stops at the envelope never sees it. Reach in

explicitly, and treat a missing field as "nothing reported":

// adapted — names generalized
private fun JsonObject.ignoredOrNull(): WriteOutcome.Ignored? {
    val ignored = this["<discard-field>"]?.jsonObject ?: return null
    val code = ignored.intOrNull("code") ?: return null
    if (code == 0) return null          // the service's "nothing was ignored" value
    return WriteOutcome.Ignored(code, ignored.stringOrNull("<text-field>").orEmpty())
}

Zero usually means "fine", not "discarded for reason zero". These fields are almost always

present on every response, carrying a neutral value on the happy path. Branching on presence rather

than on value turns every successful write into a false alarm, and the noise is what gets the check

deleted a month later.

One record comes back as an object, several as an array. Bodies translated from a

document format collapse a one-element list into a bare object. Read whichever arrived, or the

single-item case — the common one — silently skips the check:

// adapted — names generalized
val first = when (val entry = body["<items>"]) {
    is JsonArray -> entry.firstOrNull()?.jsonObject
    is JsonObject -> entry
    else -> null
}
return first?.ignoredOrNull() ?: WriteOutcome.Ok

A discard is a metadata bug wearing a network bug's clothes. The discard codes that matter are

the ones meaning "we did not like the name/identifier you sent" — those fire because *your* record

is malformed, on every submission of that record, forever. That makes the discard log the only place

a metadata defect ever becomes visible, so it belongs at warning level with the code and message in

it, next to the values that were sent:

// adapted — names generalized
is WriteOutcome.Ignored ->
    // A filtered name is how bad metadata surfaces — worth a loud log rather than a silent drop.
    Logger.w(TAG, "Write ignored (${outcome.code}): ${outcome.message}")

Not every non-Ok outcome deserves the same handling. Separate the three groups before writing

the branch, because they need opposite responses: *retryable* (rate limit, temporary outage — back

off and try again, see retry-needs-backoff-and-cap), *terminal for this credential* (the stored

session is dead — clear it and put the user back in front of the login entry), and *terminal for

this record* (filtered name, timestamp out of the accepted window — never retry, log and move on).

Retrying the third group is how a client earns a rate limit.

Mint your own code for "the call never happened". A request that failed before reaching the

service has no service code, and reusing 0 or a legal code makes the local failure indistinguishable

from a remote one. Use a value outside the range the service can produce — a negative code, where the

service's own are positive — and see unknown-not-a-valid-score for why that matters.

Recorded history: the source project's changelog records this as a finding from a live

integration — the reporting endpoint answered with a success status while discarding the submission,

and named the reason only in a side field, with the two metadata-filter codes logged loudly rather

than dropped. The record claims no rework beyond that, and in that client the check sits inside each

write method separately, which is the shape hazard worth carrying away: a write method added later

does not inherit it.

Verifying it

Find every place success is decided from the envelope alone, then check each one against the

service's own documentation for a per-record verdict:

grep -rn "isSuccess\|== 200\|status ==\|\"ok\"" --include="*.kt" . | grep -v "/build/"
grep -rniE "ignored|rejected|discarded|partial(ly)?[ _]?accept" --include="*.kt" . | grep -v "/build/"

The second command is case-insensitive on purpose: the outcome type is capitalised and the log line

is not, so a case-sensitive pattern finds the log and misses the very type this skill tells you to

declare. It should hit both. If it hits neither, every write path in the tree is reporting transport

success as semantic success.

Then verify against the service, not against your own logs: submit a record with a deliberately

malformed name, confirm the client logs a discard rather than a success, and confirm the record is

absent on the other side. A client that logs success for that submission is the exact failure this

skill exists to catch, and no amount of local testing will show it.

How to use it

Copy the folder

Take maxrave-dev/api-ok-but-ignored from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.