Pirating Pirate Data; Ducking Around With Pirate Data;
(Iโll refrain from more pirate-speak for the remainder of the post. Also, no TL;DR.)
Here I was all set to show off some Observable Notebook 2.0 features using the U.S. Gov Maritime Safety Information (MSI), Anti-Shipping Activity Messages when I noticed my dev-only R {asam} pacakge, only to discover:
> asam::read_asam()
Reading ASAM database (this may take a while)
Error in (function (quiet = FALSE) : Not Found (HTTP 404).
I assumed the MSI just did a site re-vamp,gave it a visit in the browser, saw a new link to โDownload ASAM Geospatial Filesโ which included this geodatabase, only to discover:
๐ฆ>FROM st_read('Asam_data_download.gdb') SELECT min(dateofocc), max(dateofocc);
โโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโ
โ min(dateofocc) โ max(dateofocc) โ
โ timestamp_ms โ timestamp_ms โ
โโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโค
โ 1978-05-01 00:00:00 โ 2024-06-25 17:00:54 โ
โโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโ
that the data ends in mid-2024.
O_O
I did a massive amount of Kagi-ing, and can report I have new/fresh data!
Read the first section for how to hack JWTs to access it, and the second section to see why the above example is in DuckDB.
Pirating Pirate Data

Given that it looks like the U.S. Government doesnโt care about piracy anymore (well, there were those Signal chats earlier this year, I guess), I figured this data had to be published somewhere.
It turns out Maritime Optima does publish Anti Shipping Activity Messengers (Piracy maps) | ShipAtlas User Guides in their Ship Atlas webapp/portal. You will need to sign up for a free account to access the portal, and โ thankfully โ the piracy data is one of the datasets offered in the free plan.
The section header is a BurpSuite screen capture. I used the embedded browser in it to login and spelunk the responses. Itโs manual work, but it also didnโt take too long. I just patiently followed the login flow to see where the JWT operations were, and did some response and request surgery (as youโll see, below) to automate the downloads.
NOTE: MAKE SURE TO USE USERNAME+PASSWORD vs OAuth, since youโre going to need credentials to get programmatically download the data (datasci friends donโt let datasci friends rely on manual download idioms).
Weโll be using R since the old-school {httr} library is expressive + concise, and makes it easy to login and get the data. Store your username and password in the env vars seen below and run the code:
library(httr)
# GET JWT TOKEN
c(
Host = "api.maritimeoptima.com",
`Content-Length` = "311",
`Sec-Ch-Ua-Platform` = '"macOS"',
`Local-Utc-Epoch-Time` = as.numeric(Sys.time()),
`Sec-Ch-Ua` = '"Not?A_Brand";v="99", "Chromium";v="130"',
`Accept-Language` = "en-US,en;q=0.9",
`Sec-Ch-Ua-Mobile` = "?0",
`App-Version` = "v2.227.1-master-frontend",
`App-Context` = "vessel-tracker",
`Device-Id` = "Chrome-7e87e680-f565-43e3-ade9-bb531d9debb9",
Accept = "*/*",
`Content-Type` = "application/json",
`User-Agent` = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36",
Origin = "https://app.maritimeoptima.com",
`Sec-Fetch-Site` = "same-site",
`Sec-Fetch-Mode` = "cors",
`Sec-Fetch-Dest` = "empty",
Referer = "https://app.maritimeoptima.com/",
`Accept-Encoding` = "gzip, deflate, br",
Priority = "u=1, i"
) -> login_headers
sprintf(
fmt = '{"operationName":"Login","variables":{"email":"%s","password":"%s"},"query":"mutation Login($email: String!, $password: String!, $utm: UTMInput) {\\n login(email: $email, password: $password, platform: WEB, utm: $utm) {\\n jwt\\n refresh\\n numLoggedOut\\n __typename\\n }\\n}"}',
Sys.getenv("MARITIME_OPTIMA_EMAIL"),
Sys.getenv("MARITIME_OPTIMA_PASSWORD")
) -> login_data
httr::POST(
url = "https://api.maritimeoptima.com/graphql-public",
httr::add_headers(
Host = "api.maritimeoptima.com",
`Content-Length` = "311",
`Sec-Ch-Ua-Platform` = '"macOS"',
`Local-Utc-Epoch-Time` = as.numeric(Sys.time()),
`Sec-Ch-Ua` = '"Not?A_Brand";v="99", "Chromium";v="130"',
`Accept-Language` = "en-US,en;q=0.9",
`Sec-Ch-Ua-Mobile` = "?0",
`App-Version` = "v2.227.1-master-frontend",
`App-Context` = "vessel-tracker",
`Device-Id` = "Chrome-7e87e680-f565-43e3-ade9-bb531d9debb9",
Accept = "*/*",
`Content-Type` = "application/json",
`User-Agent` = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36",
Origin = "https://app.maritimeoptima.com",
`Sec-Fetch-Site` = "same-site",
`Sec-Fetch-Mode` = "cors",
`Sec-Fetch-Dest` = "empty",
Referer = "https://app.maritimeoptima.com/",
`Accept-Encoding` = "gzip, deflate, br",
Priority = "u=1, i"
),
body = login_data
) |>
httr::content(as = "text") |>
jsonlite::fromJSON() -> login_jwt
# USE JWT TOKEN TO GET TO THE PIRATE JSON
httr::GET(
url = "https://api.maritimeoptima.com/mapdata/piracy-data.geojson",
httr::add_headers(
Host = "api.maritimeoptima.com",
Authorization = sprintf("Bearer %s", login_jwt$data$login$jwt),
`X-Team` = "587718",
`User-Agent` = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36",
Accept = "application/json",
Origin = "https://app.maritimeoptima.com",
`Sec-Fetch-Site` = "same-site",
`Sec-Fetch-Mode` = "cors",
`Sec-Fetch-Dest` = "empty",
Referer = "https://app.maritimeoptima.com/",
`Accept-Encoding` = "gzip, deflate, br",
`Accept-Language` = "en-US,en;q=0.9",
Priority = "u=1, i"
)
) |>
httr::content(as = "text") |>
writeLines("piracy-data.geojson")
Now, Iโm not thrilled we have to rely on the good graces of a commercial entity to gain access to critical safety information, but thatโs how 2025+ seems to be going all โround.
Thereโs still time to play with that data on International TLAP Day. If you donโt feel like โhackingโ, you can get the download I did in the AM on the 19th here: https://rud.is/dl/2025-09-19-piracy-data.geojson.
Ducking Around With Pirate Data

DuckDB has great support for spatial data, including GeoJSON.
If youโve ever played with the ASAM data, you may remember that it wasnโt exactly, er, consistently formatted. Youโl be glad to know that not much has changed in the GeoJSON format.
This will get you started with clean-ish data:
๐ฆ>INSTALL spatial;
๐ฆ>LOAD spatial;
๐ฆ>
๐ฆ>CREATE OR REPLACE TABLE asam AS (
FROM st_read('piracy-data.geojson')
SELECT
TO_TIMESTAMP(date) AS ts,
attack_number,
attack_type,
regexp_replace(description, '^.*Posn: [^EW]+[EW][^\w\s]+ ', '') AS description,
geom
ORDER BY ts
);
๐ฆ>
๐ฆ>FROM asam LIMIT 10;
โโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ts โ attack_number โ attack_type โ description โ geom โ
โ timestamp with timโฆ โ varchar โ varchar โ varchar โ geometry โ
โโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ 2024-09-22 07:00:0โฆ โ 078-24 โ Hijacked โ Around 47nm South โฆ โ POINT (113.833611111111โฆ โ
โ 2024-09-29 13:02:0โฆ โ 082-24 โ Boarded โ Singapore Straits.โฆ โ POINT (103.7283333333 1โฆ โ
โ 2024-10-01 12:01:0โฆ โ 081-24 โ Boarded โ Singapore Straits.โฆ โ POINT (103.7166666667 1โฆ โ
โ 2024-10-12 15:08:0โฆ โ 083-24 โ Boarded โ Kutubdia Anchorageโฆ โ POINT (91.785 21.811666โฆ โ
โ 2024-10-15 17:00:0โฆ โ 090-24 โ Boarded โ Panjang Anchorage,โฆ โ POINT (105.2875 -5.5008โฆ โ
โ 2024-10-16 15:05:0โฆ โ 104-24 โ Boarded โ Belawan Anchorage,โฆ โ POINT (98.8300715605417โฆ โ
โ 2024-10-17 12:05:0โฆ โ 086-24 โ Boarded โ Singapore Straits.โฆ โ POINT (103.49 1.13) โ
โ 2024-10-17 14:00:0โฆ โ 085-24 โ Boarded โ Singapore Straits.โฆ โ POINT (103.513333333333โฆ โ
โ 2024-10-17 14:04:0โฆ โ 088-24 โ Boarded โ Singapore Straits.โฆ โ POINT (103.7175 1.09083โฆ โ
โ 2024-10-18 18:09:0โฆ โ 084-24 โ Boarded โ Takoradi Anchorageโฆ โ POINT (-1.6865 4.878333โฆ โ
โโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ 10 rows 5 columns โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Letโs check on anti-shipping activity for this year:
๐ฆ>FROM asam
SELECT
EXTRACT(MONTH FROM ts) AS month,
MONTHNAME(ts) AS month_name,
attack_type,
COUNT(*) AS total_attacks
WHERE EXTRACT(YEAR FROM ts) = 2025
GROUP BY EXTRACT(MONTH FROM ts), MONTHNAME(ts), attack_type
ORDER BY month, attack_type;
โโโโโโโโโฌโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโ
โ month โ month_name โ attack_type โ total_attacks โ
โ int64 โ varchar โ varchar โ int64 โ
โโโโโโโโโผโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโค
โ 1 โ January โ Boarded โ 16 โ
โ 2 โ February โ Boarded โ 12 โ
โ 2 โ February โ Hijacked โ 2 โ
โ 3 โ March โ Attempted Attack โ 4 โ
โ 3 โ March โ Boarded โ 15 โ
โ 3 โ March โ Hijacked โ 2 โ
โ 4 โ April โ Attempted Attack โ 2 โ
โ 4 โ April โ Boarded โ 10 โ
โ 5 โ May โ Boarded โ 16 โ
โ 5 โ May โ Fired Upon โ 1 โ
โ 6 โ June โ Attempted Attack โ 1 โ
โ 6 โ June โ Boarded โ 17 โ
โ 7 โ July โ Attempted Attack โ 1 โ
โ 7 โ July โ Boarded โ 9 โ
โ 8 โ August โ Boarded โ 3 โ
โ 9 โ September โ Boarded โ 1 โ
โโโโโโโโโดโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโค
โ 16 rows 4 columns โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The old data from the GDB the U.S. Gov still provides does not have the same fields (so, I guess there was some cleanup), but it looks like piracy is either down, or reporting is โmehโ:
๐ฆ>CREATE OR REPLACE TABLE asam AS (
FROM st_read('Asam_data_download.gdb')
SELECT
dateofocc::DATE AS ts,
hostilitytype_l
ORDER BY ts
);
๐ฆ>
๐ฆ>FROM asam
SELECT
EXTRACT(MONTH FROM ts) AS month,
MONTHNAME(ts) AS month_name,
hostilitytype_l,
COUNT(*) AS total_attacks
WHERE EXTRACT(YEAR FROM ts) = 2024
GROUP BY EXTRACT(MONTH FROM ts), MONTHNAME(ts), hostilitytype_l
ORDER BY month, hostilitytype_l;
โโโโโโโโโฌโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโ
โ month โ month_name โ hostilitytype_l โ total_attacks โ
โ int64 โ varchar โ int16 โ int64 โ
โโโโโโโโโผโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโค
โ 1 โ January โ 2 โ 24 โ
โ 1 โ January โ 3 โ 13 โ
โ 1 โ January โ 4 โ 1 โ
โ 1 โ January โ 6 โ 6 โ
โ 1 โ January โ 7 โ 5 โ
โ 1 โ January โ 9 โ 1 โ
โ 1 โ January โ 11 โ 13 โ
โ 2 โ February โ 2 โ 20 โ
โ 2 โ February โ 3 โ 5 โ
โ 2 โ February โ 6 โ 3 โ
โ 2 โ February โ 9 โ 2 โ
โ 2 โ February โ 11 โ 9 โ
โ 3 โ March โ 1 โ 2 โ
โ 3 โ March โ 2 โ 12 โ
โ 3 โ March โ 3 โ 4 โ
โ 3 โ March โ 6 โ 5 โ
โ 3 โ March โ 7 โ 3 โ
โ 3 โ March โ 11 โ 4 โ
โ 4 โ April โ 2 โ 9 โ
โ 4 โ April โ 3 โ 4 โ
โ 4 โ April โ 4 โ 1 โ
โ 4 โ April โ 6 โ 4 โ
โ 4 โ April โ 7 โ 1 โ
โ 4 โ April โ 10 โ 1 โ
โ 4 โ April โ 11 โ 4 โ
โ 5 โ May โ 1 โ 1 โ
โ 5 โ May โ 2 โ 4 โ
โ 5 โ May โ 3 โ 4 โ
โ 5 โ May โ 4 โ 1 โ
โ 5 โ May โ 6 โ 4 โ
โ 5 โ May โ 7 โ 2 โ
โ 5 โ May โ 11 โ 10 โ
โ 6 โ June โ 2 โ 14 โ
โ 6 โ June โ 3 โ 3 โ
โ 6 โ June โ 4 โ 1 โ
โ 6 โ June โ 6 โ 1 โ
โ 6 โ June โ 11 โ 5 โ
โโโโโโโโโดโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโค
Thereโa bit of a time gap between the two data sets, and the data fields arenโt uniform, but theyโre both usable.
Maritime Optima only serves up ASAM data for a bout a ~year, so youโll need to set up a systemd timer/cron job to pull the JSON on a regular (Iโd suggest daily) basis if you do want to work with a more complete dataset next year.
FIN
Remember, you can follow and interact with the full text of The Daily Dropโs free posts on:
- ๐ Mastodon viaย
@dailydrop.hrbrmstr.dev@dailydrop.hrbrmstr.dev - ๐ฆ Bluesky viaย
https://bsky.app/profile/dailydrop.hrbrmstr.dev.web.brid.gy
โฎ๏ธ
Leave a comment