Agent Setup
End-to-end guide for AI agents, scripts, and CI/CD pipelines.
Step 1: Authenticate
Agents authenticate by signing a challenge message with a Solana keypair. No browser or wallet extension required.
Prerequisites
Generate a keypair if you don't have one:
solana-keygen new --outfile keypair.json --no-bip39-passphrase
Python (solders + requests)
import json, base64, requests
from solders.keypair import Keypair
API = "https://us-west-01-firestarter.pipenetwork.com"
kp = Keypair.from_bytes(bytes(json.load(open("keypair.json"))))
wallet = str(kp.pubkey())
# Challenge → Sign → Verify
r = requests.post(f"{API}/auth/siws/challenge",
json={"wallet_public_key": wallet})
c = r.json()
sig = kp.sign_message(c["message"].encode())
sig_b64 = base64.b64encode(bytes(sig)).decode()
tokens = requests.post(f"{API}/auth/siws/verify", json={
"wallet_public_key": wallet,
"nonce": c["nonce"],
"message": c["message"],
"signature_b64": sig_b64,
}).json()
TOKEN = tokens["access_token"]
CSRF = tokens["csrf_token"]
print("Authenticated!")Install: pip install solders requests
See full docs for Node.js and curl examples.
Step 2: Create S3 Credentials
Create an S3-compatible key pair for file operations. The secret is returned only once — save it immediately.
curl + jq
# Create S3 key (save the secret — shown only once!)
curl -s -X POST "https://us-west-01-firestarter.pipenetwork.com/api/s3/keys" \
-H "Authorization: Bearer $TOKEN" \
-H "X-CSRF-Token: $CSRF" \
-H "Content-Type: application/json" \
-d '{}' | tee pipe-s3-key.json
# Extract into env vars
export AWS_ACCESS_KEY_ID=$(jq -r .access_key_id pipe-s3-key.json)
export AWS_SECRET_ACCESS_KEY=$(jq -r .secret_access_key pipe-s3-key.json)
export AWS_ENDPOINT_URL=$(jq -r .endpoint pipe-s3-key.json)
export AWS_DEFAULT_REGION=$(jq -r .region pipe-s3-key.json)
export PIPE_S3_BUCKET=$(jq -r .bucket_name pipe-s3-key.json)Python
# After authentication (TOKEN and CSRF from step 1)
import requests
headers = {
"Authorization": f"Bearer {TOKEN}",
"X-CSRF-Token": CSRF,
"Content-Type": "application/json",
}
key = requests.post(f"{API}/api/s3/keys", headers=headers, json={}).json()
# Save immediately — secret_access_key is shown only once
print("AWS_ACCESS_KEY_ID=" + key["access_key_id"])
print("AWS_SECRET_ACCESS_KEY=" + key["secret_access_key"])
print("AWS_ENDPOINT_URL=" + key["endpoint"])
print("AWS_DEFAULT_REGION=" + key["region"])
print("PIPE_S3_BUCKET=" + key["bucket_name"])Or create keys in the portal and download the Agent Bundle .env file.
Step 3: Fund Your Account
Add credits via promo code or USDC payment. The easiest path for agents is a promo code.
Agent x402 top-ups should use your permanent user_app_key as Authorization: ApiKey <user_app_key>. The browser portal uses the separate intent → submit → poll flow instead of x402.
# Option 1: Redeem a promo code
curl -X POST "https://us-west-01-firestarter.pipenetwork.com/api/credits/redeem" \
-H "Authorization: Bearer $TOKEN" \
-H "X-CSRF-Token: $CSRF" \
-H "Content-Type: application/json" \
-d '{"code": "PIPE-XXXX-XXXX"}'
# Option 2: x402 top-up (recommended for agents)
# Export PIPE_API_KEY from /user/me first. No CSRF needed.
curl -i -X POST "https://us-west-01-firestarter.pipenetwork.com/api/credits/x402" \
-H "Authorization: ApiKey $PIPE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"amount_usdc_raw": 10000000}'
# Read the Payment-Required header (base64 JSON) to get payTo (treasury ATA),
# reference_pubkey, and intent_id. Send USDC with the reference, then confirm:
curl -i -X POST "https://us-west-01-firestarter.pipenetwork.com/api/credits/x402" \
-H "Authorization: ApiKey $PIPE_API_KEY" \
-H "Payment-Signature: BASE64_JSON_INTENT_AND_TXSIG" \
-H "Content-Type: application/json"
# If the confirm call returns 202 Accepted, poll:
curl -s "https://us-west-01-firestarter.pipenetwork.com/api/credits/intent/INTENT_ID" \
-H "Authorization: ApiKey $PIPE_API_KEY"
# Option 3: Browser portal flow (use this for web UIs, not agents)
curl -X POST "https://us-west-01-firestarter.pipenetwork.com/api/credits/intent" \
-H "Authorization: Bearer $TOKEN" \
-H "X-CSRF-Token: $CSRF" \
-H "Content-Type: application/json" \
-d '{"amount_usdc_raw": 10000000}'
curl -X POST "https://us-west-01-firestarter.pipenetwork.com/api/credits/submit" \
-H "Authorization: Bearer $TOKEN" \
-H "X-CSRF-Token: $CSRF" \
-H "Content-Type: application/json" \
-d '{"intent_id": "INTENT_ID", "tx_sig": "TX_SIGNATURE"}'Construct a USDC payment programmatically
Payment flow
Prefer Authorization: ApiKey <user_app_key> for agent and server-to-server x402 calls. Bearer + CSRF still works for compatibility, but the browser portal should use the billing intent flow instead.
- Call
POST /api/credits/x402and read thePayment-Requiredheader (base64 JSON). - Use
accepts[0]to getasset(USDC mint),payTo(treasury ATA),amount, andextra.reference_pubkey. - Build a
TransferCheckedSPL Token instruction with 6 decimals. Append thereferencepubkey as an extra readonly, non-signer account (Solana Pay pattern). - Sign and send the transaction on Solana mainnet.
- Retry
POST /api/credits/x402withPayment-Signature(base64 JSON:{"intent_id":"...","tx_sig":"..."}). - If confirm returns
202 Accepted, pollGET /api/credits/intent/{intent_id}until it becomescreditedor returns a recoverablepending + error_messagestate.
Python (solders + solana-py)
import base64, json, time, requests
import os
from solders.keypair import Keypair
from solders.pubkey import Pubkey
from solders.transaction import Transaction
from solders.message import Message
from solders.instruction import Instruction, AccountMeta
from solders.hash import Hash
from solana.rpc.api import Client
API = "https://us-west-01-firestarter.pipenetwork.com"
RPC = "https://api.mainnet-beta.solana.com"
API_KEY = os.environ["PIPE_API_KEY"]
kp = Keypair.from_bytes(bytes(json.load(open("keypair.json"))))
client = Client(RPC)
# Export PIPE_API_KEY before running.
# 1. Request x402 payment details
resp = requests.post(f"{API}/api/credits/x402",
headers={"Authorization": f"ApiKey {API_KEY}",
"Content-Type": "application/json"},
json={"amount_usdc_raw": 10_000_000})
if resp.status_code != 402:
raise Exception(resp.text)
payment_required_b64 = resp.headers["payment-required"]
payment_required = json.loads(base64.b64decode(payment_required_b64))
accept = payment_required["accepts"][0]
intent_id = accept["extra"]["intent_id"]
treasury_ata = Pubkey.from_string(accept["payTo"])
reference = Pubkey.from_string(accept["extra"]["reference_pubkey"])
usdc_mint = Pubkey.from_string(accept["asset"])
amount = int(accept["amount"]) # raw USDC (6 decimals)
# 2. Derive the sender's USDC associated token account
from spl.token.instructions import get_associated_token_address
from_ata = get_associated_token_address(kp.pubkey(), usdc_mint)
# 3. Build a TransferChecked instruction (SPL Token)
TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
keys = [
AccountMeta(from_ata, is_signer=False, is_writable=True),
AccountMeta(usdc_mint, is_signer=False, is_writable=False),
AccountMeta(treasury_ata, is_signer=False, is_writable=True),
AccountMeta(kp.pubkey(), is_signer=True, is_writable=False),
# Solana Pay reference — readonly, non-signer
AccountMeta(reference, is_signer=False, is_writable=False),
]
import struct
data = struct.pack("<BQB", 12, amount, 6)
ix = Instruction(TOKEN_PROGRAM, data, keys)
# 4. Send transaction
blockhash = Hash.from_string(client.get_latest_blockhash().value.blockhash.__str__())
msg = Message.new_with_blockhash([ix], kp.pubkey(), blockhash)
tx = Transaction.new_unsigned(msg)
tx.sign([kp], blockhash)
sig = client.send_transaction(tx).value
print(f"TX signature: {sig}")
# 5. Retry with Payment-Signature
payload = base64.b64encode(json.dumps({"intent_id": intent_id, "tx_sig": str(sig)}).encode()).decode()
confirm = requests.post(f"{API}/api/credits/x402",
headers={"Authorization": f"ApiKey {API_KEY}",
"Content-Type": "application/json", "Payment-Signature": payload},
)
if confirm.status_code == 202:
while True:
time.sleep(int(confirm.headers.get("Retry-After", "2")))
detail = requests.get(
f"{API}/api/credits/intent/{intent_id}",
headers={"Authorization": f"ApiKey {API_KEY}"}
)
detail.raise_for_status()
snapshot = detail.json()
if snapshot["status"] == "credited":
print("Credits added!")
break
if snapshot["status"] == "pending" and snapshot.get("error_message"):
raise Exception(snapshot["error_message"])
else:
confirm.raise_for_status()
print("Credits added!")Install: pip install solders solana spl-token-py requests
Node.js (@solana/web3.js + @solana/spl-token)
import { Connection, PublicKey, Transaction } from "@solana/web3.js";
import { createTransferCheckedInstruction, getAssociatedTokenAddress } from "@solana/spl-token";
import { Keypair } from "@solana/web3.js";
import fs from "fs";
const API = "https://us-west-01-firestarter.pipenetwork.com";
const RPC = "https://api.mainnet-beta.solana.com";
const API_KEY = process.env.PIPE_API_KEY;
if (!API_KEY) {
throw new Error("Set PIPE_API_KEY before running this example.");
}
const kp = Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(fs.readFileSync("keypair.json", "utf-8")))
);
const connection = new Connection(RPC, "confirmed");
// 1. Request x402 payment details
const x402Resp = await fetch(`${API}/api/credits/x402`, {
method: "POST",
headers: {
"Authorization": `ApiKey ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ amount_usdc_raw: 10_000_000 }),
});
if (x402Resp.status !== 402) {
throw new Error(await x402Resp.text());
}
const paymentRequiredHeader = x402Resp.headers.get("payment-required");
if (!paymentRequiredHeader) throw new Error("Missing PAYMENT-REQUIRED header");
const paymentRequired = JSON.parse(Buffer.from(paymentRequiredHeader, "base64").toString("utf-8"));
const accept = paymentRequired.accepts[0];
const intentId = accept.extra.intent_id;
const mint = new PublicKey(accept.asset);
const treasuryAta = new PublicKey(accept.payTo);
const reference = new PublicKey(accept.extra.reference_pubkey);
// 2. Derive sender's USDC ATA
const fromAta = await getAssociatedTokenAddress(mint, kp.publicKey);
// 3. Build TransferChecked + Solana Pay reference
const ix = createTransferCheckedInstruction(
fromAta, mint, treasuryAta, kp.publicKey,
BigInt(accept.amount), 6
);
ix.keys.push({ pubkey: reference, isSigner: false, isWritable: false });
// 4. Send transaction
const { blockhash } = await connection.getLatestBlockhash("confirmed");
const tx = new Transaction().add(ix);
tx.feePayer = kp.publicKey;
tx.recentBlockhash = blockhash;
tx.sign(kp);
const sig = await connection.sendRawTransaction(tx.serialize());
await connection.confirmTransaction(sig, "confirmed");
console.log("TX signature:", sig);
// 5. Retry with Payment-Signature
const paymentSignature = Buffer.from(JSON.stringify({ intent_id: intentId, tx_sig: sig }), "utf-8").toString("base64");
const confirmResp = await fetch(`${API}/api/credits/x402`, {
method: "POST",
headers: {
"Authorization": `ApiKey ${API_KEY}`,
"Payment-Signature": paymentSignature,
"Content-Type": "application/json",
},
});
if (confirmResp.status === 202) {
while (true) {
const waitMs = (Number(confirmResp.headers.get("Retry-After")) || 2) * 1000;
await new Promise((resolve) => setTimeout(resolve, waitMs));
const detailResp = await fetch(`${API}/api/credits/intent/${intentId}`, {
headers: {
"Authorization": `ApiKey ${API_KEY}`,
},
});
if (!detailResp.ok) {
throw new Error(await detailResp.text());
}
const snapshot = await detailResp.json();
if (snapshot.status === "credited") break;
if (snapshot.status === "pending" && snapshot.error_message) {
throw new Error(snapshot.error_message);
}
}
} else if (!confirmResp.ok) {
throw new Error(await confirmResp.text());
}
console.log("Credits added!");Install: npm install @solana/web3.js @solana/spl-token
Or fund via the portal UI with wallet-based USDC payment.
Step 4: Test Your Setup
Source your credentials and verify everything works.
# List bucket contents aws s3 ls s3://<your-bucket> --endpoint-url <s3-endpoint> # Upload a test file echo "hello from agent" > /tmp/agent-test.txt aws s3 cp /tmp/agent-test.txt s3://<your-bucket>/agent-test.txt \ --endpoint-url <s3-endpoint> # Download it back aws s3 cp s3://<your-bucket>/agent-test.txt /tmp/agent-test-dl.txt \ --endpoint-url <s3-endpoint> cat /tmp/agent-test-dl.txt # Verify via REST API curl -H "Authorization: ApiKey YOUR_API_KEY" \ "https://us-west-01-firestarter.pipenetwork.com/download-stream?file_name=agent-test.txt"
Full Automation Script
One-shot bash script that authenticates, creates S3 keys, and writes an agent bundle file. Requires curl, jq, and solana CLI.
#!/usr/bin/env bash
# pipe-agent-setup.sh — one-shot setup for headless environments
# Requires: curl, jq, solana CLI (for signing)
set -euo pipefail
API="https://us-west-01-firestarter.pipenetwork.com"
KEYPAIR="./keypair.json"
# 1. Get wallet pubkey
WALLET=$(solana-keygen pubkey "$KEYPAIR")
echo "Wallet: $WALLET"
# 2. Challenge
CHALLENGE=$(curl -s -X POST "$API/auth/siws/challenge" \
-H "Content-Type: application/json" \
-d "{\"wallet_public_key\":\"$WALLET\"}")
MESSAGE=$(echo "$CHALLENGE" | jq -r .message)
NONCE=$(echo "$CHALLENGE" | jq -r .nonce)
# 3. Sign (using solana CLI)
SIG_B64=$(echo -n "$MESSAGE" | solana sign --keypair "$KEYPAIR" --output base64)
# 4. Verify
TOKENS=$(curl -s -X POST "$API/auth/siws/verify" \
-H "Content-Type: application/json" \
-d "{\"wallet_public_key\":\"$WALLET\",\"nonce\":\"$NONCE\",\"message\":\"$MESSAGE\",\"signature_b64\":\"$SIG_B64\"}")
TOKEN=$(echo "$TOKENS" | jq -r .access_token)
CSRF=$(echo "$TOKENS" | jq -r .csrf_token)
echo "Authenticated!"
# 5. Get API key
API_KEY=$(curl -s "$API/user/me" -H "Authorization: Bearer $TOKEN" | jq -r .user_app_key)
# 6. Create S3 key
S3KEY=$(curl -s -X POST "$API/api/s3/keys" \
-H "Authorization: Bearer $TOKEN" \
-H "X-CSRF-Token: $CSRF" \
-H "Content-Type: application/json" -d '{}')
# 7. Write agent bundle
cat > pipe-agent-bundle.env <<EOF
PIPE_API_BASE=$API
PIPE_API_KEY=$API_KEY
AWS_ACCESS_KEY_ID=$(echo "$S3KEY" | jq -r .access_key_id)
AWS_SECRET_ACCESS_KEY=$(echo "$S3KEY" | jq -r .secret_access_key)
AWS_DEFAULT_REGION=$(echo "$S3KEY" | jq -r .region)
AWS_ENDPOINT_URL=$(echo "$S3KEY" | jq -r .endpoint)
PIPE_S3_BUCKET=$(echo "$S3KEY" | jq -r .bucket_name)
EOF
echo "Done! Credentials saved to pipe-agent-bundle.env"
echo " source pipe-agent-bundle.env"
echo " aws s3 ls s3://\$PIPE_S3_BUCKET --endpoint-url \$AWS_ENDPOINT_URL"Error Handling & Token Refresh
Agents must handle three failure modes: expired tokens (401), rate limits (429), and invalid CSRF tokens (403).
Token refresh flow
The access_token is a short-lived JWT (typically minutes). When it expires, exchange your refresh_token for a new pair:
POST /auth/refresh
{ "refresh_token": "your_refresh_token" }
→ { "access_token": "...", "csrf_token": "..." }The refresh_token itself is longer-lived. If refreshing fails with 401, re-authenticate from scratch (challenge → sign → verify). Store your refresh token securely — treat it like a password.
Python — robust API client
import time, requests
def refresh_tokens(api, refresh_token):
"""Exchange a refresh token for a new access + CSRF token."""
r = requests.post(f"{api}/auth/refresh", json={
"refresh_token": refresh_token,
})
r.raise_for_status()
data = r.json()
return data["access_token"], data.get("csrf_token", "")
def api_call(api, method, path, token, csrf="", **kwargs):
"""Make an API call with automatic 401 refresh and 429 backoff."""
headers = {"Authorization": f"Bearer {token}"}
if method.upper() != "GET":
headers["X-CSRF-Token"] = csrf
headers["Content-Type"] = "application/json"
for attempt in range(3):
r = requests.request(method, f"{api}{path}", headers=headers, **kwargs)
if r.status_code == 401:
# Token expired — caller should refresh and retry
raise TokenExpiredError()
if r.status_code == 429:
wait = int(r.headers.get("Retry-After", 2 ** attempt))
print(f"Rate limited, waiting {wait}s...")
time.sleep(wait)
continue
if r.status_code == 403 and "csrf" in r.text.lower():
raise CSRFError("CSRF token invalid — re-authenticate")
r.raise_for_status()
return r.json()
raise Exception("Max retries exceeded")
class TokenExpiredError(Exception): pass
class CSRFError(Exception): passNode.js — robust API client
async function apiCall(api, method, path, token, csrf = "") {
const headers = { "Authorization": `Bearer ${token}` };
if (method !== "GET") {
headers["X-CSRF-Token"] = csrf;
headers["Content-Type"] = "application/json";
}
for (let attempt = 0; attempt < 3; attempt++) {
const resp = await fetch(`${api}${path}`, { method, headers });
if (resp.status === 401) throw new Error("TOKEN_EXPIRED");
if (resp.status === 429) {
const wait = Number(resp.headers.get("Retry-After")) || 2 ** attempt;
console.log(`Rate limited, waiting ${wait}s...`);
await new Promise(r => setTimeout(r, wait * 1000));
continue;
}
if (resp.status === 403) {
const body = await resp.text();
if (body.toLowerCase().includes("csrf")) throw new Error("CSRF_INVALID");
throw new Error(`Forbidden: ${body}`);
}
if (!resp.ok) throw new Error(`API error ${resp.status}: ${await resp.text()}`);
return resp.json();
}
throw new Error("Max retries exceeded");
}
async function refreshTokens(api, refreshToken) {
const resp = await fetch(`${api}/auth/refresh`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refresh_token: refreshToken }),
});
if (!resp.ok) throw new Error("Refresh failed — re-authenticate from scratch");
return resp.json(); // { access_token, csrf_token }
}Retry strategy
- 401 Unauthorized — token expired. Refresh using
POST /auth/refresh, then retry once. If refresh also 401s, re-authenticate fully. - 429 Too Many Requests — respect the
Retry-Afterheader (seconds). Use exponential backoff with jitter as fallback:2^attempt + random(0,1)seconds. - 403 Forbidden — if the body mentions "csrf", your CSRF token is stale. Re-authenticate to get a fresh one.
- 5xx Server Error — retry with backoff, up to 3 attempts.
Bootstrap Your Config
After authenticating, assemble your full working config from these 4 endpoints. Run them in parallel to minimize latency.
| Endpoint | Returns |
|---|---|
| GET /user/me | user_app_key, wallet, account_state |
| GET /api/s3/info | endpoint, region |
| GET /api/s3/bucket | bucket_name, public_read, cors |
| GET /api/credits/status | balance_usdc, total_deposited, intent |
bash (parallel curl)
# Fetch all config in one shot (4 parallel requests) TOKEN="your_access_token" API="https://us-west-01-firestarter.pipenetwork.com" # Run in parallel with & curl -s "$API/user/me" -H "Authorization: Bearer $TOKEN" > /tmp/me.json & curl -s "$API/api/s3/info" -H "Authorization: Bearer $TOKEN" > /tmp/s3info.json & curl -s "$API/api/s3/bucket" -H "Authorization: Bearer $TOKEN" > /tmp/bucket.json & curl -s "$API/api/credits/status" -H "Authorization: Bearer $TOKEN" > /tmp/credits.json & wait # Assemble config cat <<EOF PIPE_API_BASE=$API PIPE_API_KEY=$(jq -r .user_app_key /tmp/me.json) AWS_ENDPOINT_URL=$(jq -r .endpoint /tmp/s3info.json) AWS_DEFAULT_REGION=$(jq -r .region /tmp/s3info.json) PIPE_S3_BUCKET=$(jq -r .bucket_name /tmp/bucket.json) PIPE_BALANCE=$(jq -r .balance_usdc /tmp/credits.json) EOF
Python (concurrent)
import requests
from concurrent.futures import ThreadPoolExecutor
API = "https://us-west-01-firestarter.pipenetwork.com"
headers = {"Authorization": f"Bearer {TOKEN}"}
def get(path):
return requests.get(f"{API}{path}", headers=headers).json()
# Fetch all config in parallel
with ThreadPoolExecutor(max_workers=4) as pool:
me_f = pool.submit(get, "/user/me")
s3info_f = pool.submit(get, "/api/s3/info")
bucket_f = pool.submit(get, "/api/s3/bucket")
credits_f = pool.submit(get, "/api/credits/status")
config = {
"api_base": API,
"api_key": me_f.result()["user_app_key"],
"endpoint": s3info_f.result()["endpoint"],
"region": s3info_f.result()["region"],
"bucket": bucket_f.result()["bucket_name"],
"balance_usdc": credits_f.result()["balance_usdc"],
}
print(config)CI/CD: GitHub Actions
Upload build artifacts to Pipe Storage on every push. Add your S3 credentials as GitHub repository secrets.
Required secrets
Go to your repo → Settings → Secrets and variables → Actions, then add:
- PIPE_AWS_ACCESS_KEY_ID
- PIPE_AWS_SECRET_ACCESS_KEY
- PIPE_AWS_REGION
- PIPE_AWS_ENDPOINT_URL
- PIPE_S3_BUCKET
All values are in your Agent Bundle .env file from the S3 Keys panel.
.github/workflows/upload.yml
name: Pipe Storage Upload
on:
push:
branches: [main]
jobs:
upload:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install AWS CLI
run: |
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o awscli.zip
unzip -q awscli.zip && sudo ./aws/install
- name: Upload to Pipe Storage
env:
AWS_ACCESS_KEY_ID: ${{ secrets.PIPE_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.PIPE_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.PIPE_AWS_REGION }}
AWS_ENDPOINT_URL: ${{ secrets.PIPE_AWS_ENDPOINT_URL }}
PIPE_S3_BUCKET: ${{ secrets.PIPE_S3_BUCKET }}
run: |
# Upload build artifacts (example: dist/ folder)
aws s3 sync ./dist s3://$PIPE_S3_BUCKET/releases/$GITHUB_SHA/ \
--endpoint-url $AWS_ENDPOINT_URL
# Verify upload
aws s3 ls s3://$PIPE_S3_BUCKET/releases/$GITHUB_SHA/ \
--endpoint-url $AWS_ENDPOINT_URLAPI Contract Reference
Key behaviors and limits agents need to know. These apply to all API interactions.
Account creation
Accounts are created automatically on first successful /auth/siws/verify. There is no separate signup endpoint. A new wallet = a new account with an empty bucket and zero balance.
Authentication methods
Authorization: Bearer <access_token>— for all endpoints. Short-lived JWT. Pair withX-CSRF-Tokenon mutations (POST/PATCH/DELETE).Authorization: ApiKey <user_app_key>— preferred for agent/headless calls, including x402 credits, plus file operations. Permanent, no CSRF needed. Get it from/user/me.
Token lifetimes
- access_token — short-lived JWT. Check the
expclaim (Unix seconds) to know when it expires. Refresh proactively ~30s before expiry. - refresh_token — longer-lived. Use
POST /auth/refreshto get new tokens. If refreshing fails with 401, re-authenticate fully. - csrf_token — returned with every token pair. Required on all mutation requests.
- user_app_key — permanent until rotated via
POST /rotateAppKey.
Error response format
All API errors return JSON with one of these shapes:
// Standard error
{ "error": "Human-readable error message" }
// Or with field-level detail
{ "message": "Validation failed", "details": { ... } }
// Rate limit (429)
HTTP 429 + Retry-After: 5
{ "error": "Rate limit exceeded" }On 401, the body may be empty or contain {"error":"token expired"}. Always check the HTTP status code first, then parse the body.
Rate limits
- Rate limits are enforced per-account. When exceeded, the API returns
429with aRetry-Afterheader (seconds). - Recommended strategy: exponential backoff with jitter. Start at 1s, double each retry, add random 0–1s jitter, max 3 retries.
- Avoid tight polling loops. Use reasonable intervals: 5s+ for status checks, 30s+ for activity polling.
Bucket model
- Each account gets a default bucket, automatically created on first auth. With prepaid credits, you can create additional buckets.
- Query bucket name and settings with
GET /api/s3/bucket. - Toggle public read and CORS origins with
PATCH /api/s3/bucket. - The bucket persists until the account is purged.
S3 key prefix enforcement
When creating an S3 key, the response includes a name_prefix field. If non-empty, all S3 operations with that key are scoped to objects under that prefix. Attempting to read or write outside the prefix will return 403. The default key has an empty prefix (full bucket access).
S3 compatibility & limits
- Supported: PutObject, GetObject, HeadObject, DeleteObject, ListObjectsV2, CopyObject.
- Multipart uploads: supported but individual parts may be limited. For very large files (>5 GB), use smaller part sizes or chunked uploads.
- DeleteObjects (batch): not supported. Delete objects one at a time with
DeleteObject. - Path-style required: AWS SDK v3 (JS/TS) needs
forcePathStyle: true. boto3 and the AWS CLI use path-style by default with custom endpoints. - S3 keys don't expire. Revoke them explicitly with
DELETE /api/s3/keys/<id>.
Account states
active— normal operation. All endpoints work.frozen— credits exhausted or policy violation. Reads work, writes are blocked. Top up credits to unfreeze.purge_scheduled— data deletion is scheduled. Top up or contact support to cancel.purged— all data has been deleted. Account is effectively closed.
Check account state with GET /user/me → account_state.
OpenAPI specification
The full OpenAPI 3.0 spec is available at https://us-west-01-firestarter.pipenetwork.com/openapi.yaml — no authentication required. Use it to auto-generate API clients in any language. Note: auth endpoints are included in the raw spec but hidden in the portal's Redoc view.
No webhooks (yet)
There are no webhook or callback endpoints. To detect upload completion, poll GET /api/user/activity?limit=5 at reasonable intervals (every 5–10s). Each entry includes status ("completed" / "failed"), operation_type, bytes_transferred, and started_at.