diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
index 4d3f906..2b4727c 100644
--- a/.github/workflows/pull-request.yml
+++ b/.github/workflows/pull-request.yml
@@ -10,10 +10,10 @@ jobs:
name: Test build on PR
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-node@v2
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
with:
- node-version: 14.x
+ node-version: 18.x
cache: yarn
- name: Test build
run: |
diff --git a/docs/pinning-service/ipfs-pinning-service-api.md b/docs/pinning-service/ipfs-pinning-service-api.md
index e98217c..1c169ef 100644
--- a/docs/pinning-service/ipfs-pinning-service-api.md
+++ b/docs/pinning-service/ipfs-pinning-service-api.md
@@ -78,3 +78,114 @@ For more commands and general help:
```bash
ipfs pin remote --help
```
+
+## Pinning a DAG (recursive pinning)
+
+In the IPFS Pinning Service API, **pinning a CID is recursive DAG pinning**. There is no separate "DAG pin" operation and no recursive flag — every pin request pins the *entire* DAG rooted at the CID.
+
+The [IPFS Pinning Services API Spec](https://ipfs.github.io/pinning-services-api-spec/) defines the `cid` of a pin as:
+
+> Content Identifier (CID) points at the root of a DAG that is pinned recursively.
+
+So when you pin `bafybeib32…` (a file, a directory, or any IPLD/UnixFS DAG root), Functionland Fula retrieves and pins **every block reachable from that root**. A single CID is all you need to pin an arbitrarily large DAG — the commands in the previous section (`ipfs pin remote add …`) are already performing recursive DAG pins.
+
+How the service obtains the DAG (per the standard): after you submit a CID, the service finds providers for the involved CIDs across the IPFS network — optionally helped by the `origins` hints you supply — and downloads the DAG over Bitswap. This is why a pin you just created starts in `queued`/`pinning` and only becomes `pinned` once the whole DAG has been fetched.
+
+## Standard REST API
+
+The `ipfs` CLI above is a convenience wrapper over the standard REST endpoints. You can call them directly against `https://api.cloud.fx.land` with your access token as a bearer credential. Functionland Fula implements the full IPFS Pinning Services API spec.
+
+| Operation | Endpoint | Description |
+|---|---|---|
+| Add pin | `POST /pins` | Request a recursive DAG pin for a CID |
+| List pins | `GET /pins` | List/filter pin objects (by `cid`, `name`, `status`, `before`/`after`, `meta`) |
+| Get pin | `GET /pins/{requestid}` | Check the status of one pin request |
+| Replace pin | `POST /pins/{requestid}` | Atomically remove + re-add (avoids GC of blocks common to both pins) |
+| Remove pin | `DELETE /pins/{requestid}` | Remove a pin object |
+
+All requests send `Authorization: Bearer YOUR_JWT`.
+
+**Create a pin (recursive DAG pin):**
+
+```bash
+curl -X POST "https://api.cloud.fx.land/pins" \
+ -H "Authorization: Bearer YOUR_JWT" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "cid": "bafybeib32tuqzs2wrc52rdt56cz73sqe3qu2deqdudssspnu4gbezmhig4",
+ "name": "war-and-peace.txt"
+ }'
+```
+
+The **Pin** object you send accepts: `cid` (required — the DAG root), `name` (optional label), `origins` (optional multiaddrs the service should try first to fetch the data), and `meta` (optional string-to-string key/value metadata).
+
+The service responds with a **PinStatus** object:
+
+```json
+{
+ "requestid": "UniqueIdOfPinRequest",
+ "status": "queued",
+ "created": "2024-06-28T15:02:42Z",
+ "pin": {
+ "cid": "bafybeib32tuqzs2wrc52rdt56cz73sqe3qu2deqdudssspnu4gbezmhig4",
+ "name": "war-and-peace.txt"
+ },
+ "delegates": ["/dnsaddr/.../p2p/Qm..."],
+ "info": {}
+}
+```
+
+**Pin lifecycle:** `queued` → `pinning` → `pinned` (or `failed`). Poll `GET /pins/{requestid}` until the status settles:
+
+```bash
+curl "https://api.cloud.fx.land/pins/UniqueIdOfPinRequest" \
+ -H "Authorization: Bearer YOUR_JWT"
+```
+
+**List your pins.** `GET /pins` returns a paginated `PinResults` object — a `count` of total matches plus a `results` array of `PinStatus` objects. To page through more than one batch, pass the `before` filter with the `created` timestamp of the oldest item you've seen:
+
+```bash
+curl "https://api.cloud.fx.land/pins?status=pinned&limit=10" \
+ -H "Authorization: Bearer YOUR_JWT"
+```
+
+```json
+{ "count": 42, "results": [ { "requestid": "…", "status": "pinned", "pin": { "cid": "…" } } ] }
+```
+
+To speed up the transfer when your own node already has the data, put your node's multiaddrs in `origins`, and pre-connect to the peers returned in the response's `delegates` array (see [Provider hints](https://ipfs.github.io/pinning-services-api-spec/#section/Provider-hints) in the spec).
+
+## Importing a DAG by uploading a CAR file
+
+This is a **Functionland extension**, not part of the IPFS Pinning Service API standard. The standard is pin-by-CID only and has no data-upload endpoint — it assumes the DAG can be fetched from the network. CAR import fills the gap when the data exists **only on your machine** and no other peer is providing it yet.
+
+The same content-addressed convention used by [web3.storage](https://web3.storage), NFT.storage and Filebase: you package your DAG as a [CAR (Content Addressable aRchive)](https://ipld.io/specs/transport/car/) file and upload it; the service imports the blocks and recursively pins the root CID to your account.
+
+**1. Export your DAG to a CAR file** (using your local IPFS/Kubo node):
+
+```bash
+# Pin or add your data locally first, then export its DAG to a CAR:
+ipfs dag export bafybeib32tuqzs2wrc52rdt56cz73sqe3qu2deqdudssspnu4gbezmhig4 > data.car
+```
+
+**2. Upload the CAR** to the import endpoint (multipart `file`, optional `name`):
+
+```bash
+curl -X POST "https://api.cloud.fx.land/pins/import/car" \
+ -H "Authorization: Bearer YOUR_JWT" \
+ -F "file=@data.car" \
+ -F "name=my-dataset"
+```
+
+The service validates the CAR, imports its blocks into the Fula network, and recursively pins the root. It returns the same **PinStatus** object as `POST /pins` (with `status: "queued"`), so you track it the same way via `GET /pins/{requestid}` until it becomes `pinned`. Re-importing a CAR whose root you already have simply returns your existing pin.
+
+**Requirements and limits:**
+
+- **Single root** — the CAR must have exactly one root CID (the DAG that gets pinned). Multi-root CARs are rejected.
+- **Complete DAG** — every block referenced by the DAG must be present in the CAR. A partial DAG is rejected (it could never finish pinning).
+- **Formats** — CARv1 and CARv2 are both accepted (the `ipfs dag export` default is CARv1).
+- **Integrity** — every block's bytes are verified against its CID on the way in.
+- **Size** — up to 800 MB per CAR by default; blocks above 2 MiB are rejected.
+- **Quota** — the imported DAG counts against your storage quota; an import that wouldn't fit your plan is rejected up front with `402`.
+
+**Error responses:** `400` invalid CAR (bad/truncated, hash mismatch, zero or multiple roots, missing blocks, unsupported codec, oversized/over-deep block); `402` insufficient storage credit; `413` CAR larger than the allowed maximum; `429` too many concurrent imports. The response body carries the specific reason.