# ArcNautical API — agent rules

Drop this file in your repo root. Your agent will pick it up on every run.

Reference: https://arcnautical.com/llms-full.txt (guide) ·
https://arcnautical.com/api/v1/openapi.json (contract) ·
https://arcnautical.com/developers/ (human docs)

Base URL: `https://arcnautical.com/api/v1`

## Screening a hull needs no key

```
GET /api/v1/vessels/{imo}/check
```

No account, no credentials, 100 requests/hour per IP. Returns live sanctions status
(RED / AMBER / GREEN across OFAC SDN, OpenSanctions, EU, UN, UK), ownership opacity, and a
vetting grade. If `coverageComplete` is `false`, a source was unreachable — report that
rather than a clean result, and list `coverageGaps`.

## Resolving a port needs no key

```
GET /api/v1/ports?query=rotterdam&limit=5
```

Returns `{"ports":[{"locode","name","country","country_code","lat","lon"}]}`, best match
first. Covers every port the routing engine can compute against, so a `locode` from here is
always routable. An exact code matches itself, so this also confirms one you already hold.

## Voyage scoring needs a key

Mint one at https://arcnautical.com/arcnautical.html#/developer-api — both environments are
self-serve, no approval. Live: 10,000 assessments/month. Test: 1,000. Same engine, same
data; the environment only selects the quota bucket, idempotency namespace and webhook
namespace.

```
POST /api/v1/voyage-assessments
Authorization: Bearer $ARCNAUTICAL_API_KEY
Idempotency-Key: <uuid>
Content-Type: application/json

{"route": {"origin": "SGSIN", "destination": "NLRTM", "vessel_type": "container"}}
```

Returns 201 with `score`, `risk_level`, `drivers`, `confidence`, `source_status`,
`missing_sources`, `methodology_version`, `disclaimer`, `assessed_at`.

## Rules

1. **`Idempotency-Key` is required on every POST, PATCH and DELETE.** Missing it returns
   `{"code":"missing_idempotency_key",...}`. Fresh UUID per distinct request, same UUID when
   retrying that request.
2. **`origin` / `destination` accept two shapes** — a 5-character LOCODE string (`"SGSIN"`)
   or an object (`{"locode":"SGSIN"}`). Both are correct. Do not rewrite one into the other.
   **Never guess a LOCODE.** If the user gave you a port name, resolve it with
   `GET /api/v1/ports?query=<name>` first. A guessed code is either rejected as unknown, or —
   worse — is a real port somewhere else, and you report a confident score for the wrong
   voyage. Port names are not accepted in `origin`/`destination` precisely to stop that.
3. **429 has two causes, handled differently.** `rate_limit_exceeded` = over 120 req/min on
   the key; back off to `X-RateLimit-Reset`. `concurrency_limit` = more than 2 assessments in
   flight for this customer; stop parallelising and retry shortly. Neither is permanent.
4. **Key prefix must match environment** — `arc_test_` or `arc_live_`. A mismatch is 401.
5. **Errors are always JSON** `{code, message, request_id}`. An HTML body means a network
   layer answered, not the API — retry rather than concluding it is broken, and quote
   `request_id` if you report it.
6. **A single assessment embeds the full route geometry** (hundreds of coordinates). Read
   `score`, `risk_level`, `drivers`, `confidence` — do not echo the whole payload to a user.
7. **Batches** take up to 10 routes, return 202, and are polled at
   `/api/v1/voyage-assessment-batches/{id}` — do not fire 10 single assessments in parallel,
   that trips the concurrency limit.
8. **A score is decision support, not a determination.** Report it with its `confidence` and
   `assessed_at`, name anything in `missing_sources`, and never present it as a navigational,
   insurance, or sanctions-compliance determination.
