# Autonomous Agents Network (AANet)

> Multi-agent coordination outside your LLM context: dedupe parallel work, per-agent budgets, handoff

- name: space.aanet/aanet-mcp
- version: 1.3.0
- connection class: R0 (autonomous)
- trust: 83/100 flags: no-repository
- quality: 72/100 (needs work)
- owner: not claimed
- descriptor: https://api.protogrid.dev/v1/servers/space.aanet%2Faanet-mcp
- tools: https://api.protogrid.dev/v1/servers/space.aanet%2Faanet-mcp/tools

**An agent can connect right now, no human step.** Remote endpoint, no authentication, reachable on the last probe.

## Connection (mcpServers)

```json
{
  "mcpServers": {
    "aanet-mcp": {
      "type": "http",
      "url": "https://aanet.space/mcp"
    }
  }
}
```


## Trust

- hygiene: 80
- liveness: 100
- freshness: 100
- provenance: 55
- drivers: +dns-namespace +website +reachable +uptime-100% +updated-12d-ago -no-repository
- Derived from observable signals (official registry feed and our own credential-free probes); no code audit performed.

## Quality 72/100 (needs work)

### protocol: 88 (weight 25)

- pass `protocol.modern`: supports 2026-07-28 (server/discover)
- pass `protocol.stateless`: answers without a session
- pass `protocol.transport`: streamable HTTP
- warn `protocol.list_ttl`: tools/list declares ttlMs 0, so clients re-fetch it on every use

### authorization: n/a (weight 15)

- n/a `auth.prm`, `auth.as_metadata`, `auth.cimd`: no remote uses OAuth
- n/a `auth.secret_in_url`: no templated URL

### tool hygiene: 57 (weight 30)

- pass `tools.descriptions`: every tool has a description
- warn `tools.description_length`: 1 tools have descriptions over 1,024 characters, which crowds the context
- pass `tools.schemas`: every tool has an object input schema
- fail `tools.annotations`: no tool declares readOnlyHint or destructiveHint, so clients cannot tell safe calls from risky ones
- fail `tools.directory_hints`: no tool declares all four of readOnlyHint, destructiveHint, idempotentHint and openWorldHint; missing: aanet_write_file (readOnlyHint, destructiveHint, idempotentHint, openWorldHint); aanet_read_file (readOnlyHint, destructiveHint, idempotentHint, openWorldHint); aanet_submit_feedback (readOnlyHint, destructiveHint, idempotentHint, openWorldHint); …
- warn `tools.token_cost`: about 12,099 tokens to load every tool
- pass `tools.api_dump`: tools are not a one-to-one API dump

### stability: 75 (weight 15)

- warn `stability.changes`: 23 tool changes in 30 days
- pass `stability.rug_pull`: no tool changed its meaning under the same name

### dependencies: n/a (weight 15)

- n/a `deps.known_vulns`, `deps.mcp_sdk_version`, `deps.resolvable`: no npm or PyPI package

- tools: 31, about 12,099 tokens to load them all
- tool set hash: 83f416cc2a18b3896e3ee7c22d14d8d8
- tools last changed: 2026-10-09T15:18:26.623Z
- badge: [![protogrid quality](https://protogrid.dev/badge/space.aanet/aanet-mcp.svg)](https://protogrid.dev/servers/space.aanet/aanet-mcp)
- Checks run on what our credential-free, read-only probes observe; tools are never called and no code audit is performed.

### Tool changes

- 2026-10-09 `aanet_register_webhook` changed (inputSchema)
- 2026-10-09 `aanet_get_forecast` added
  - Burn rate and runway: the workspace balance, spend per hour over the last 1h / 24h / 7d (each window shortened to the workspace's age if it is younger) with the hours the balance would last at that pace, and the top spending sub-keys of the last 24h. Each window states the interval it was measured over (measured_since, covered_seconds, shortened) — a young workspace gives a short span and so a large hourly figure. An estimate from the audit log, not a guarantee. Owner_key required; free. Pair it with a balance_low webhook (aanet_register_webhook) to be warned before a 402.
- 2026-10-08 `aanet_get_webhook_deliveries` changed (description)
  - before: Inspect one webhook's delivery history — use this to debug a webhook that seems to have gone quiet (check aanet_list_webhooks first to see if it's been auto-disabled) or to confirm an event actually fired. Owner_key required. Each entry has event_type, status_code (null if the request itself never completed, e.g. timeout or DNS failure), and success.
  - after: Inspect one webhook's delivery history — use this to debug a webhook that seems to have gone quiet (check aanet_list_webhooks first to see if it's been auto-disabled) or to confirm an event actually fired. Owner_key required. Each entry has event_type, status_code (null if the request itself never completed, e.g. timeout or DNS failure), success, and skipped (true = nothing was sent because the workspace couldn't afford the delivery: not billed, not an endpoint failure).
- 2026-10-08 `aanet_list_webhooks` changed (description)
  - before: List webhooks registered on a workspace — url, subscribed events, and whether each is still active or auto-disabled after repeated failures. Owner_key required. Secrets are never included (only shown once, at aanet_register_webhook time); use aanet_get_webhook_deliveries for per-attempt delivery history on one of these.
  - after: List webhooks registered on a workspace — url, subscribed events, consecutive_failures (failed attempts since the last success), and disabled_at/disabled_reason if it was auto-disabled after 10 failed attempts in a row (you also get an aanet.system message when that happens; deliveries skipped for an empty balance don't count). Owner_key required. Secrets are never included (only shown once, at aanet_register_webhook time); use aanet_get_webhook_deliveries for per-attempt delivery history on one of these.
- 2026-10-08 `aanet_register_webhook` changed (description)
  - before: Register a push endpoint so this workspace calls you instead of you calling aanet_get_activity/aanet_get_messages/aanet_acquire_lock in a polling loop — the main alternative to polling anywhere in this API. Owner_key required; up to 5 active webhooks per workspace. Returns {webhook_id, secret} with secret shown exactly once — every delivery includes an X-AANet-Signature header (HMAC-SHA256 of the raw body, keyed by that secret); verify it before trusting a payload as genuinely from us. You are billed 2 units for every delivery attempt that is actually made, whether or not your endpoint responds (visible in aanet_get_activity); an attempt the workspace can't afford is skipped, recorded as failed, and retried. A webhook that fails 10 times in a row is auto-disabled — check aanet_get_webhook_deliveries if events stop arriving. Delivery comes from a durable outbox that survives a service restart and retries failures with exponential backoff (up to 8 attempts); it is at-least-once, so dedupe on the X-AANet-Delivery-Id header. Still treat a webhook as a faster way to learn something happened, not your only source of truth — confirm with the matching GET tool if it matters.
  - after: Register a push endpoint so this workspace calls you instead of you calling aanet_get_activity/aanet_get_messages/aanet_acquire_lock in a polling loop — the main alternative to polling anywhere in this API. Owner_key required; up to 5 active webhooks per workspace. Returns {webhook_id, secret} with secret shown exactly once — every delivery includes an X-AANet-Signature header (HMAC-SHA256 of the raw body, keyed by that secret); verify it before trusting a payload as genuinely from us. You are billed 2 units for every delivery attempt that is actually made, whether or not your endpoint responds (visible in aanet_get_activity); an attempt the workspace can't afford is not made and not billed — it is listed as skipped, doesn't count as a failure and is retried every 15 minutes for up to 24 hours. A webhook that fails 10 delivery attempts in a row is auto-disabled (you get an aanet.system message and aanet_list_webhooks shows disabled_reason) — check aanet_get_webhook_deliveries if events stop arriving. Delivery comes from a durable outbox that survives a service restart and retries failures with exponential backoff (up to 8 attempts); it is at-least-once, so dedupe on the X-AANet-Delivery-Id header. Still treat a webhook as a faster way to learn something happened, not your only source of truth — confirm with the matching GET tool if it matters.
- 2026-10-08 `aanet_get_messages` changed (inputSchema)
- 2026-10-08 `aanet_send_message` changed (inputSchema)
- 2026-10-08 `aanet_get_activity_summary` added
  - Spend totals from the audit log instead of reading every entry: for each group, total_cost (units) and calls. Use it to see which sub-agent is burning budget before adjusting a budget_cap via aanet_update_subkey. Owner_key required. An unknown group_by is an error.
- 2026-10-08 `aanet_get_activity` changed (inputSchema)
- 2026-10-08 `aanet_get_lock_status` added
  - Check a lock WITHOUT trying to take it, and tell "nobody ever took it" apart from "someone took it and released it" — a free lock and an already-finished task look identical from aanet_acquire_lock alone. Returns status held | expired (lease ran out, not yet reclaimed) | free (taken before, not held now) | never_held, plus holder_subkey_id/lease_expires_at while held, last_holder_subkey_id, last_acquired_at, last_released_at, acquire_count and release_count. A release is a signal the holder finished with it, not proof the work succeeded — confirm via the result file. 1 unit; a name nobody ever acquired is a free miss.

Full history: https://api.protogrid.dev/v1/servers/space.aanet%2Faanet-mcp/changes

## Remotes

- https://aanet.space/mcp (streamable-http, auth none, reachable true, uptime30d 1)

## Packages

_none_

## Tools (31)

- `aanet_acquire_lock`: Claim exclusive ownership of a named unit of work so no other
    sub-agent starts the same thing — call this before starting work, not
    after. Returns {lease_expires_at} on success, or a 409 if another
    sub-agent already holds an unexpired lease on the same name (that
    conflict is free — you are not charged for losing a race).

    You do not hold the lock forever: once lease_seconds elapses, anyone can
    acquire it out from under you, even if you're still working. Call
    aanet_renew_lock periodically while still working, and
    aanet_release_lock the moment you're done so others don't wait out the
    full lease unnecessarily.
- `aanet_append_file`: Append one entry to a file without touching what's already there,
    creating the file if it doesn't exist yet — use aanet_write_file instead
    when you need to replace the whole file. Typical use: a shared log or
    running notes file multiple sub-agents contribute to. Fires a
    file_write webhook event on success. Rejected with 413, before writing
    or charging, if the resulting file (existing content + this append)
    would exceed 1 MiB. Returns {path, appended, etag}.
- `aanet_create_trial_workspace`: Create a free trial workspace — no payment, no arguments, works
    immediately. Use this instead of aanet_create_workspace when you want to
    exercise the API (files, locks, sub-keys, messages) before committing
    real money.

    Pre-funded with a small fixed balance (currently 1000 units = $0.01 — enough
    for a realistic multi-agent round, but NOT for a successful write of a
    file at the 1 MiB cap, which costs 1029 units; that is a known limit of
    the trial, not a bug. An oversize write is still rejected with 413 for
    free, so the cap itself is checkable) and
    expires exactly 24 hours after creation, regardless of remaining
    balance or activity — it is then deleted permanently along with
    everything in it. Returns the same shape as aanet_create_workspace plus
    trial_expires_at. aanet_deposit and aanet_request_refund both reject a
    trial workspace outright, since no real money ever entered it. If your
    task won't fit in 24 hours, create a real workspace instead.
- `aanet_create_workspace`: Create a real AANet workspace with a prepaid USDC balance — the entry
    point for an orchestrator agent starting a new swarm task. Use
    aanet_create_trial_workspace instead if you want to explore the API
    before paying anything.

    On success, returns {workspace_id, label, owner_key, created_at}. Store
    owner_key immediately: it is generated once, never stored in recoverable
    form, and there is no "forgot my key" recovery path — losing it means
    losing control of the workspace. The workspace starts with a balance of
    0; fund it with aanet_deposit before any metered operation will succeed.
- `aanet_delete_file`: Permanently remove a file — the missing piece of the file lifecycle
    alongside aanet_read_file/aanet_write_file/aanet_append_file. There is
    no undo: to recover, a sub-agent would need its own backup of the
    content. Fires a file_delete webhook event on success (see
    aanet_register_webhook). Raises a 404 if the path doesn't exist, a 409
    if it's a directory or if_match doesn't match the current etag. Returns
    {path, deleted}.
- `aanet_delete_webhook`: Permanently remove a registered webhook — no further events will be
    delivered to it, active or auto-disabled. Owner_key required. There is
    no way to temporarily pause one; delete and re-register with
    aanet_register_webhook if you want it back later (you'll get a new
    secret).
- `aanet_delete_workspace`: Permanently and irreversibly delete a workspace: every sub-key, all
    files, the activity log, and any registered webhooks are destroyed
    along with it — there is no undo and no trash/recycle bin. Requires the
    owner_key; a sub-key gets a 403.

    Call aanet_request_refund first if the workspace still has balance you
    want back, and wait for aanet_get_refund_status to show "paid" before
    deleting — once the workspace is gone, so is any way to claim what was
    left. Use this when a task is finished or permanently abandoned, not as
    a way to "reset" a workspace you plan to keep using.
- `aanet_deposit`: Fund a real workspace via the x402 protocol — a two-step call using
    the same tool both times. This does not accept trial workspaces (use
    aanet_create_workspace for a fundable one) and requires the owner_key.

    Step 1 — call with only api_key/workspace_id/amount_usd: returns an x402
    PaymentRequired challenge offering USDC on Base, Polygon, Arbitrum,
    Avalanche, and Solana as payment options; nothing is charged yet.
    Step 2 — sign that challenge
    with your own wallet/x402 client (this tool cannot sign for you) and
    call again with the identical amount_usd plus the resulting proof as
    payment_signature; on success this verifies and settles against the
    facilitator and returns {workspace_id, deposited_units, tx_hash,
    network}. A failed or already-used signature raises an error rather than
    silently no-op'ing.
- `aanet_estimate_savings`: Estimate the cost of coordinating a swarm on AANet versus relaying the
    same coordination through your orchestrator's LLM context — before you
    create a workspace or spend anything. Same formula as the calculator at
    https://aanet.space/#savings.

    AANet side assumes ~16 units/round (the average measured over an 8-round
    run: reads, appends, a lock cycle, a message and a poll) at $0.00001/unit. The relay side assumes ~9,600 tokens/round
    (re-injecting another agent's update into your prompt) at a blended
    $2-$10 per million token rate. This is a rough estimate, not a quote —
    real workloads vary; see aanet_get_activity on an actual workspace for
    your own measured cost once you're running for real.
- `aanet_get_activity`: Read the server-side audit log of every attempt in this workspace,
    successful or rejected — the authoritative source of what each
    sub-agent actually did, since sub-agents can't fake or skip this the
    way they could misreport their own actions. Owner_key required.

    Each entry has subkey_id, operation, path, cost_charged, status (e.g.
    OK, INSUFFICIENT_BALANCE, SCOPE_DENIED, CONFLICT), and a free-text
    detail. Use this to reconstruct what happened after the fact, or to
    check a sub-agent's spend pattern before adjusting its budget_cap via
    aanet_update_subkey.
- `aanet_get_activity_summary`: Spend totals from the audit log instead of reading every entry: for each
    group, total_cost (units) and calls. Use it to see which sub-agent is
    burning budget before adjusting a budget_cap via aanet_update_subkey.
    Owner_key required. An unknown group_by is an error.
- `aanet_get_forecast`: Burn rate and runway: the workspace balance, spend per hour over the
    last 1h / 24h / 7d (each window shortened to the workspace's age if it is
    younger) with the hours the balance would last at that pace, and the top
    spending sub-keys of the last 24h. Each window states the interval it was
    measured over (measured_since, covered_seconds, shortened) — a young workspace
    gives a short span and so a large hourly figure. An estimate from the audit
    log, not a guarantee. Owner_key required; free. Pair it with a balance_low webhook
    (aanet_register_webhook) to be warned before a 402.
- `aanet_get_lock_status`: Check a lock WITHOUT trying to take it, and tell "nobody ever took it"
    apart from "someone took it and released it" — a free lock and an
    already-finished task look identical from aanet_acquire_lock alone. Returns
    status held | expired (lease ran out, not yet reclaimed) | free (taken before,
    not held now) | never_held, plus holder_subkey_id/lease_expires_at while held,
    last_holder_subkey_id, last_acquired_at, last_released_at, acquire_count and
    release_count. A release is a signal the holder finished with it, not proof
    the work succeeded — confirm via the result file. 1 unit; a name nobody ever
    acquired is a free miss.
- `aanet_get_messages`: Fetch messages sent via aanet_send_message — the pull-based fallback
    for when you haven't registered a webhook on the message event. A
    sub-key only sees messages addressed to it plus broadcasts (to_subkey_id
    was left unset by the sender); the owner_key sees every message in the
    workspace, for oversight. There's no read/unread tracking server-side —
    track your own last-seen ts and pass it as `since` on the next call,
    the same pattern as aanet_get_activity.
- `aanet_get_refund_status`: Check the status of every refund request ever made for a workspace —
    the way to find out whether an aanet_request_refund call actually got
    paid. Owner_key required. Each entry has status ("pending", "paid", or
    "rejected"), and tx_hash once paid — verify that transaction on-chain
    yourself before treating the refund as confirmed and deleting the
    workspace. A "rejected" entry means the reserved amount was credited
    back to the workspace balance, not lost.
- `aanet_get_remaining_balance`: Free (never charged): how much this key can still spend, so you can
    stop before a 402 instead of discovering it mid-sequence — the MCP
    counterpart of the REST X-AANet-Remaining-Balance header. Takes only the
    key; works for the owner_key or any sub-key, against itself.

    Returns `remaining` (what you can actually spend: the lower of the two
    limits below), `key_budget_remaining` (this key's budget_cap minus what it
    has spent; null if the key has no cap), and `workspace_balance` (the
    shared workspace balance), all in raw integer units ($0.00001 each).
    Comparing the two limits tells you which one runs out first.
- `aanet_get_webhook_deliveries`: Inspect one webhook's delivery history — use this to debug a webhook
    that seems to have gone quiet (check aanet_list_webhooks first to see
    if it's been auto-disabled) or to confirm an event actually fired.
    Owner_key required. Each entry has event_type, status_code (null if the
    request itself never completed, e.g. timeout or DNS failure), success, and
    skipped (true = nothing was sent because the workspace couldn't afford the
    delivery: not billed, not an endpoint failure).
- `aanet_get_workspace`: Check a workspace's current balance and status — the read-only
    counterpart to every other workspace tool. Works with the owner_key or
    any sub-key issued under it (not restricted to the owner).

    Returns deposit_balance and total_spent in raw integer units ($0.00001
    each), plus is_trial/trial_expires_at (null for a non-trial workspace)
    and blocked (true if the operator has manually suspended it, in which
    case every metered call returns 403 until unblocked). Call this before a
    batch of expensive operations to confirm you won't hit a 402 partway
    through.
- `aanet_list_subkeys`: List every sub-key ever issued under a workspace (including revoked
    ones), with each one's scope, budget_cap, and spent_so_far — the
    inventory view that complements aanet_get_activity's per-event log.
    Owner_key required; raw key values are never included since they're
    only ever shown once at creation.
- `aanet_list_webhooks`: List webhooks registered on a workspace — url, subscribed events,
    consecutive_failures (failed attempts since the last success), and
    disabled_at/disabled_reason if it was auto-disabled after 10 failed
    attempts in a row (you also get an aanet.system message when that happens;
    deliveries skipped for an empty balance don't count). Owner_key required.
    Secrets are never included (only shown once, at aanet_register_webhook
    time); use aanet_get_webhook_deliveries for per-attempt delivery history
    on one of these.
- `aanet_mint_subkey`: Issue a new scoped credential for one sub-agent — the standard way to
    bring a sub-agent into an existing workspace. Requires the owner_key; a
    sub-key cannot mint another sub-key.

    Returns {subkey_id, key} where key is shown exactly once, the same as
    owner_key at workspace creation. Give each sub-agent its own sub-key
    rather than sharing one across several — that's what makes
    aanet_get_activity attributable per sub-agent and what lets you revoke
    exactly one misbehaving sub-agent (aanet_revoke_subkey) without taking
    down the rest of the swarm.
- `aanet_read_file`: Read a file's content, or list a directory's entries if path points
    to one — the read counterpart to aanet_write_file/aanet_append_file.
    Costs 1 unit per hit (file or directory); a missing path returns 404 and is free. Returns {path, content,
    is_dir, entries, etag}; for a directory, content is null and entries
    lists child names, for a file it's the reverse. Save the returned etag
    if you plan to write back — pass it as if_match on aanet_write_file to
    avoid clobbering a newer write from another sub-agent.
- `aanet_register_webhook`: Register a push endpoint so this workspace calls you instead of you
    calling aanet_get_activity/aanet_get_messages/aanet_acquire_lock in a
    polling loop — the main alternative to polling anywhere in this API.
    Owner_key required; up to 5 active webhooks per workspace.

    Returns {webhook_id, secret} with secret shown exactly once — every
    delivery includes an X-AANet-Signature header (HMAC-SHA256 of the raw
    body, keyed by that secret); verify it before trusting a payload as
    genuinely from us. You are billed 2 units for every delivery attempt that
    is actually made, whether or not your endpoint responds (visible in
    aanet_get_activity); an attempt the workspace can't afford is not made
    and not billed — it is listed as skipped, doesn't count as a failure and
    is retried every 15 minutes for up to 24 hours. A webhook that fails 10
    delivery attempts in a row is auto-disabled (you get an aanet.system
    message and aanet_list_webhooks shows disabled_reason) — check
    aanet_get_webhook_deliveries if events stop arriving. Delivery comes from a durable outbox that survives a service
    restart and retries failures with exponential backoff (up to 8
    attempts); it is at-least-once, so dedupe on the X-AANet-Delivery-Id
    header. Still treat a webhook as a faster way to learn something
    happened, not your only source of truth — confirm with the matching GET
    tool if it matters.
- `aanet_release_lock`: Voluntarily give up a lock you hold, immediately, instead of waiting
    for its lease to expire — always call this as soon as you're done with
    the unit of work, so a sub-agent waiting on it doesn't idle
    unnecessarily. Free (no charge), unlike acquire/renew. Fails with 404 if
    nobody holds it or 403 if you're not the current holder. Fires a
    lock_available webhook event on success (see aanet_register_webhook) so
    a waiting sub-agent can react immediately instead of polling
    aanet_acquire_lock in a loop.
- `aanet_renew_lock`: Extend a lock you currently hold, before its lease runs out — call
    this instead of aanet_acquire_lock again while work is still in
    progress. Fails with 404 if nobody holds the lock, or 403 if a
    different sub-key is the current holder (you can't renew someone
    else's lease). Returns {lease_expires_at}.
- `aanet_request_refund`: Ask for a workspace's entire remaining balance back, when its task is
    done and money is left over — call this before aanet_delete_workspace,
    not after. Owner_key required; trial workspaces reject this outright
    (they never held real money).

    This reserves (zeros out) the full balance immediately, so a second call
    with nothing left to refund fails rather than double-paying out. The
    real network fee for the payout is deducted from what you receive
    (net_payout_usd in the response = gross - fee); if the balance doesn't
    even cover the estimated fee, this call fails instead of reserving
    anything. Payout is processed manually by the operator, not instantly —
    poll aanet_get_refund_status for the outcome rather than assuming
    success once this call returns.
- `aanet_revoke_subkey`: Permanently revoke one sub-agent's credentials — every subsequent call
    with that key gets a 401. Use this when a sub-agent is compromised,
    misbehaving, or simply finished, without affecting any other sub-key in
    the workspace. Owner_key required.

    Immediately releases any lock the revoked sub-key was holding (as if it
    had called aanet_release_lock itself), so a revoked sub-agent can't
    accidentally block the rest of the swarm by holding a lock forever.
- `aanet_send_message`: Send a one-shot signal directly to another sub-agent (or to
    everyone), for things that don't belong in a file — e.g. "chunk 3
    done" or "aborting, don't wait on me". Use aanet_write_file /
    aanet_append_file instead for anything that needs to persist as durable
    state; messages are meant to be cheap and disposable, not a record of
    truth.

    Cheaper than a file write on purpose. Fires a message webhook event on
    success (see aanet_register_webhook) — pair the two instead of having
    the recipient poll aanet_get_messages in a loop. Returns
    {message_id, ts}.
- `aanet_submit_feedback`: Report a bug, request a feature, or flag that pricing feels wrong —
    this is the one feedback channel into how the operator prioritizes what
    to build next, not a support ticket that gets a reply. Any valid,
    unrevoked key can call this, owner or sub-key. Returns
    {feedback_id, status} with status always "open" initially; there is no
    tool to check its status afterward — it either got through (you got a
    result back) or it didn't (you got an error).
- `aanet_update_subkey`: Change a live sub-key's scope and/or budget cap without revoking and
    reissuing it — use this instead of aanet_revoke_subkey + aanet_mint_subkey
    when the sub-agent's credentials themselves should keep working (e.g.
    widening its file scope mid-task, or raising a budget cap it hit).
    Owner_key required. Fields left unset are unchanged, not cleared.
- `aanet_write_file`: Overwrite a file's entire content, creating it if it doesn't exist —
    use aanet_append_file instead if you want to add one entry without
    replacing what's there. Fires a file_write webhook event on success
    (see aanet_register_webhook).

    Pass if_match with the file's last-known etag to make this a
    compare-and-swap: if another sub-agent wrote to the same path since you
    last read it, this raises a 409 instead of silently overwriting their
    change — re-read, resolve, and retry rather than assume your write won.
    content over 1 MiB is rejected with 413, before writing or charging —
    this service is for coordination artifacts, not bulk storage.
    Returns {path, created, etag}.
