← sluice

API reference

REST access to Filecoin Onchain Cloud on the Calibration testnet. Sluice never holds your private key — you keep your USDFC in your own Filecoin Pay account and grant Sluice bounded operator rights on-chain. Amounts are decimal USDFC strings, never floats.

Testnet only. You need tFIL for gas and tUSDFC to spend, both from public faucets, before anything here will work.

Two gates, and how they differ

Sluice sits behind two independent limits, and it is worth knowing which one you are hitting.

  • 1Your API key cap — enforced by Sluice. Under it, a request executes immediately. Over it, the request is held and you decide.
  • 2Your on-chain operator allowance — enforced by the Filecoin Pay contract, not by Sluice. This is the hard ceiling on everything Sluice can ever commit, and no bug on our side can exceed it.

The grant that creates the second limit is a single transaction you sign in the dashboard:

solidity
// What you sign once, in the browser, from your own wallet.
// After this an agent can spend — but only within these numbers,
// and the contract, not Sluice, is what enforces them.

setOperatorApproval(
  USDFC,
  sluiceOperatorAddress,
  true,
  rateAllowance,     // max USDFC per epoch
  lockupAllowance,   // max USDFC ever committed  <- the real ceiling
  maxLockupPeriod
)

// Revoke at any time:
setOperatorApproval(USDFC, sluiceOperatorAddress, false, 0, 0, 0)

Crucially, withdraw and withdrawTo revert with CallerNotPayer. An operator can direct your funds through payment rails but can never pull them out to itself.

Agents authenticate with Authorization: Bearer sluice_sk_…. Keys are stored hashed and are minted from the dashboard. GET /verify/:pieceCid, /health and /stats need no key at all — they are read-only and spend nothing.

POST /api/v1/pay/authorize

The gate. Sluice prices the request against the chain beforeanything is signed, then compares it to your key's cap.

  • 200Under the cap. Already broadcast; the response carries the transaction hash.
  • 202Over the cap. Held as pending_approval. Nothing has been signed — poll /pay/status/:id.
bash
curl -X POST https://your-sluice.vercel.app/api/v1/pay/authorize   -H "Authorization: Bearer sluice_sk_..."   -H "Content-Type: application/json"   -H "Idempotency-Key: run-42-payment"   -d '{"kind": "pay", "to": "0xRecipient...", "amount": "0.25"}'

Pass Idempotency-Key on anything you might retry. A replay returns the original authorization instead of paying twice; reusing a key with a different body is a 409.

Payment kinds

Filecoin Pay is rail-based rather than peer-to-peer, so a payment opens a channel and then moves a lump sum through it. Sluice does this as your operator, in one call.

json
# One-time payment. Creates a rail if you do not name one.
{"kind": "pay", "to": "0xRecipient...", "amount": "0.5"}

# Reuse the rail from a previous payment — saves a transaction.
{"kind": "pay", "to": "0xRecipient...", "amount": "0.5", "railId": "17"}

# Change a rail's streaming rate (USDFC per epoch).
{"kind": "modify_rate", "railId": "17", "ratePerEpoch": "0.001"}

# Close a rail.
{"kind": "terminate_rail", "railId": "17"}

# Store data in Warm Storage; returns a PieceCID for /verify.
{"kind": "store", "dataBase64": "aGVsbG8gZmlsZWNvaW4=", "label": "run-42"}

The first payment to a recipient creates a rail and costs an extra transaction. Keep the reusableRailId from the response and pass it as railId next time.

GET /api/v1/pay/status/:id

The polling endpoint. Also where a broadcast transaction gets confirmed and a stale request gets expired, so your poll loop drives the whole lifecycle. Watch the pending boolean and stop when it goes false.

POST /api/v1/pay/approve/:id

The human side. Requires the Firebase ID token of the account whose funds are at stake — an API key is deliberately not enough, since the agent being gated must not be able to release its own payment. In practice you click the button in the control room.

Body: { "decision": "approve" } or { "decision": "reject" }.

What the API deliberately cannot do

deposit, withdraw, settle, and granting or revoking operator access are not available over REST. This is not a policy choice: Filecoin Pay restricts them to the account holder, so Sluice could not perform them for you even if it wanted to.

They live in the dashboard, signed by your own wallet. Asking for one over REST returns a 400 that says so.

GET /api/v1/verify/:pieceCid

PDP proof status as plain JSON. No wallet, no key, no SDK. Proofs are submitted per data set rather than per piece, so the timestamps describe the data set containing the piece.

bash
curl https://your-sluice.vercel.app/api/v1/verify/bafkzcibcd4bdomn3tgwgrh3g532zopskstnbrd2n3sxfqbppqmw2vpv3g7dtoq

{
  "pieceCid": "bafkzcibcd4bdomn...",
  "dataSetId": "42",
  "healthy": true,
  "status": "healthy",
  "dataSetLastProven": "2026-07-24T09:12:00.000Z",
  "dataSetNextProofDue": "2026-07-24T21:12:00.000Z",
  "isProofOverdue": false,
  "inChallengeWindow": false,
  "retrievalUrl": "https://...",
  "pieceId": "3"
}

This takes a PieceCID (bafkzc…), not an IPFS CID. You get one back from a store authorization. Add ?client=0x…to check data sets owned by another address; it defaults to the gateway's own wallet.

Full example — Python

No JS runtime, no wallet handling, no SDK. Just requests.

python
import os, time, requests

SLUICE = os.environ["SLUICE_URL"]
AUTH = {"Authorization": f"Bearer {os.environ['SLUICE_API_KEY']}"}

def authorize(payload, idempotency_key=None):
    headers = dict(AUTH)
    if idempotency_key:
        headers["Idempotency-Key"] = idempotency_key
    r = requests.post(f"{SLUICE}/api/v1/pay/authorize", json=payload, headers=headers)
    r.raise_for_status()
    return r.json()["authorization"]

def wait(auth_id, timeout=600):
    """Block until terminal.

    A 202 means the account owner has to approve first, so this can
    legitimately sit here for minutes. Poll politely.
    """
    deadline = time.time() + timeout
    while time.time() < deadline:
        r = requests.get(f"{SLUICE}/api/v1/pay/status/{auth_id}", headers=AUTH)
        r.raise_for_status()
        a = r.json()["authorization"]
        if not a["pending"]:
            return a
        time.sleep(3)
    raise TimeoutError(f"{auth_id} still pending after {timeout}s")

PAYEE = "0xRecipient..."

# 1. Under the cap: executes immediately.
small = authorize({"kind": "pay", "to": PAYEE, "amount": "0.25"})
print(small["status"], small.get("txHash"))

# Keep the rail id — reusing it saves a transaction next time.
rail = (small.get("result") or {}).get("reusableRailId")

# 2. Over the cap: held until the account owner approves.
big = authorize({"kind": "pay", "to": PAYEE, "amount": "12.0", "railId": rail})
print(big["status"])          # pending_approval
final = wait(big["id"])
print(final["status"], final.get("explorerUrl"))

# 3. Verify a stored piece — no key needed for this one.
piece = requests.get(f"{SLUICE}/api/v1/verify/bafkzcib...").json()
print(piece["status"], piece["dataSetLastProven"])

No-code — n8n HTTP node

Header auth with your Sluice key, and the execution id as the idempotency key so a re-run never double-pays.

json
{
  "method": "POST",
  "url": "https://your-sluice.vercel.app/api/v1/pay/authorize",
  "authentication": "genericCredentialType",
  "genericAuthType": "httpHeaderAuth",
  "sendHeaders": true,
  "headerParameters": {
    "parameters": [
      { "name": "Idempotency-Key", "value": "={{ $execution.id }}" }
    ]
  },
  "sendBody": true,
  "specifyBody": "json",
  "jsonBody": "={{ JSON.stringify({ kind: 'pay', to: '0xRecipient...', amount: '0.25' }) }}"
}

Errors

Every failure returns { "error": { "code", "message" } }. Branch on code — it is stable; the message is not.

401 unauthorized       Missing or unknown API key
403 forbidden          Key revoked, operator access not granted or revoked,
                       or out of on-chain allowance
404 not_found          No such authorization (or it belongs to another key)
409 conflict           Idempotency-Key reused with a different body,
                       or the request is no longer awaiting approval
429 too_many_requests  Daily budget exhausted, or another payment is
                       mid-signature — retry in a few seconds
502 upstream_error     The chain or a storage provider rejected the call