# 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 / INCOMPLETE / 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`.

Start here. It returns the verdict only; a production integration uses a free key (below).

## 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.

## A production integration needs a free key

The user mints it: it takes a browser sign-up, so you cannot. Free, no card, no approval
step, about a minute:

https://arcnautical.com/arcnautical.html?intent=api&utm_source=agents_md&utm_campaign=get_a_key#/register

Ask them to set it as `ARCNAUTICAL_API_KEY` in the environment, never in a committed file.
A new key carries every scope. Live: 2,000 screenings once on a new account (a one-time
allowance that does not reset) and 10,000 voyage assessments a month. Test: 500 screenings
and 1,000 voyage assessments a month. Same engine, same data; the environment only selects the quota
bucket, idempotency namespace and webhook namespace.

The key returns the full screening record with evidence (every match with its list,
programme and confidence), kept as a citable record, plus batches of 50 hulls, vessel
monitors and signed webhooks.

### Screen a hull

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

{"imo": "9811000"}
```

Returns 201 with `sanctions` (`status`, `coverage_complete`, `coverage_gaps`, `matches`),
`ownership`, `vetting`, `sources`, `methodology_version`, `screened_at`. `INCOMPLETE` means
a mandatory list or the hull's identity was unavailable: it is not a clean result, and it
is not charged.

### Score a voyage, on the same key

```
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 three causes, handled differently.** `rate_limit_exceeded` = over 120 req/min
   on the key; back off to `X-RateLimit-Reset`. `concurrency_limit` = you are over your OWN
   in-flight ceiling (2 assessments, or 6 screenings); stop parallelising and retry shortly.
   `service_concurrency_limit` = the shared pool is full and your account is within its
   allowance; cutting your fan-out will not help, just retry. None 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.
