Agent API · v1 · Policies

How the pipes behave

The Agent API is meant to be boring: the same request gets the same answer, a failed call costs nothing, and nothing changes without a dated entry on the changelog. This page states the rules the code enforces. Every number on it is rendered from the constant the route reads, so it cannot drift from what you will actually hit.

The machine-readable version is GET /api/v1/status. The contract itself (modes, prices, schemas, error codes) is on the API reference.

Versioning

The contract is a versioned public interface under /api/v1. Both rails, coaches at POST /api/v1/agents/{agent_id}/call and actions at POST /api/v1/actions/{slug}/call, follow one rule:

The request and response schemas are the single source: the published JSON Schema, the OpenAPI document and the API reference are generated from the same definitions the routes validate against, so the docs cannot describe a field the route does not check.

Deprecation

Nothing has been deprecated. When something is, this is what happens:

Prices

Prices can change, and the changelog records each change as kind pricing. What settles is exactly the amount your wallet signed. The exact-payment scheme's facilitator refuses a payment whose signed value differs from the amount required when it verifies, so a price that moves between your 402 and your paid retry produces a fresh 402 carrying the new amount, never a settlement at an amount you did not sign. A price change is never applied to a receipt after the fact.

Rate limits

Payment is the real rate limiter. These exist to bound a client stuck in a loop, not to shape use, and every one of them answers with a 4xx or 5xx before anything settles, so tripping a limit never costs you a payment. Honour Retry-After when it is present.

LimitValueWhen you exceed it
Calls per address600 per 10 minutes, sliding429 rate_limited with Retry-After set to the window (600 seconds). One bucket for coach and action calls. Too many requests from your address. Honour the Retry-After header.
Sandbox calls per address200 per 10 minutes, slidingThe same 429, from a separate bucket. A request carrying x-gardenpatch-sandbox never counts against the paid allowance, so free traffic cannot starve a paying caller, including two customers behind one address.
Characters per call, Triage tier3,200 across messages and context400 input_too_large before a price is quoted; the body names the cap and the count. Your prompt exceeds the character cap the tier pays for. Shorten it, or call a higher tier.
Characters per call, Artifact tier6,500 across messages and context400 input_too_large before a price is quoted; the body names the cap and the count. Your prompt exceeds the character cap the tier pays for. Shorten it, or call a higher tier.
Characters per call, Operating doc tier33,000 across messages and context400 input_too_large before a price is quoted; the body names the cap and the count. Your prompt exceeds the character cap the tier pays for. Shorten it, or call a higher tier.
Idempotency-Key length255 characters400 invalid_idempotency_key, before a price is quoted.
Identical request already runningOne at a time per key409 request_in_flight with a short Retry-After; come back and collect the deliverable. If the first call dies mid-flight its claim frees itself within 6 minutes, so nothing can lock you out of your own key for long.
Daily inference ceilingSet per deployment, not published503 capacity. We are shedding load. Honour Retry-After — it is set to an hour, not seconds.

The per-address numbers are environment-tunable and GET /api/v1/status reports the live ones. When the limit store is unreachable the coach rail fails open (a paying caller is served without the limit) and the action rail fails closed (503 idempotency_unavailable, nothing sent, nothing charged), because a duplicate text to a real customer cannot be recalled and a lost sale can.

What we store, and for how long

Two things are stored about a paid call, both in a key-value store with an expiry set at write time. There is no database of calls, and there is no copy of what you sent.

WhatHow longWhy
Your request bodyNot storedIt is reduced to a SHA-256 fingerprint of the canonical JSON, which is one input to the idempotency key and is kept only as part of the replay record below. Your messages and context are sent to the model provider to produce the answer and are not written anywhere by us.
Replay record, under an Idempotency-Key you sent24 hoursThe response we returned, its status, your payer address, the scheme and network, the settlement header, the price, the request fingerprint and timestamps. This is what an identical retry is served from instead of a second run and a second charge.
Replay record, under a derived key10 minutesSame record, when you sent no key and we derived one from your wallet and body. Shorter because a derived key is a guess at intent: it exists to catch a transport retry, which happens within seconds, not to decide that an identical call hours later was a mistake.
Receipt7 daysThe same record, keyed by the rcpt_ id in your paid response. Free to read at GET /api/v1/receipts/{id}, never cached, 404 after expiry. Longer than the replay window because it serves a ledger, not a retry. Read receipt.expires_at first: when it is null, nothing was stored.
In-flight markerat most 6 minutesThe claim on your key while a call runs, replaced by the replay record on success and deleted on any refusal.
Your address10 minutesThe rate-limit window is a set of request timestamps keyed by your network address. It expires with the window and holds nothing else.
Demand counters35 daysOne counter per mode and outcome per UTC day (sandbox, quoted, paid, rejected, failed), so we can see what people want. No caller identity, no bodies, no prompts. A daily aggregate of our own inference spend is kept the same way, with no caller identity.

Server logs carry warnings and errors: the mode id, the attempt number, and the failure reason (an output that was truncated, a schema miss, a store write that failed, an upstream error). They never carry your messages, your context or the deliverable.

Actions

For an action the replay record holds the response (the action id, the send service's request_id, the channel, queued, the business name) and the fingerprint of the request. The customer's contact details and the consent block are not stored on our side in clear. They travel with the request to the send service, which keeps them with the send record: the consent block (basis, obtained_at, source) is required precisely so that a record exists of who agreed to what and when, if a carrier ever asks. Retention there is that service's; the request_id is how either side references the send.

One deliberate exception: a send whose outcome we could not confirm (send_state: "unknown") is stored under your key for the replay window so that a retry replays that answer instead of texting the customer a second time. A different Idempotency-Key is how you say the second send is intended.

Charges and refunds

Refunds

Settlement is an on-chain USDC transfer and there is no automated reversal. If a settled call returned something defective (a body that fails its own published schema, or an empty deliverable), send the receipt id to hello@gardenpatch.xyz. A refund is a manual USDC transfer to the payer address on the receipt, and the receipt is what we check, which is why it is worth keeping.

Support and disclosure

Write to hello@gardenpatch.xyz. A person reads it. Include what lets us find the call without your request body:

Do not send your request body unless we ask for it; the receipt id is enough to trace a call.

Security issues go to the same address with security in the subject; we would rather hear about one than read about it. Requests about personal data you or your customers sent go to privacy@gardenpatch.xyz, as described in the privacy policy.

Status

GET /api/v1/status is free, cacheable for a minute, and open to any origin. It is computed from the same functions the paid routes gate on, not from a separate opinion:

curl -sS https://gardenpatch.xyz/api/v1/status

Found a place where the code and this page disagree? The code is the contract and the page is the bug. Tell us.