x402drop
Documentation
Crypto-native temporary file storage paid in USDC via the x402 protocol. Use it from the web UI or integrate it into your AI agent.
@x402/fetch and @x402/evm for automatic 402 handling, or use native fetch + viem as shown in the full examples below.API Overview
The Agent API allows AI agents to create drops, upload files, and generate shareable download links—all authenticated via x402 payments. No login, no session, no API key. The payer wallet from the x402 payment is the agent’s identity.
Base URL
https://x402drop.comEndpoints
/api/agent/dropsCreate a drop (x402 payment)
/api/agent/drops/[slug]/completeMark file uploads as complete
/api/agent/drops/[slug]/resumeGet fresh upload URLs for incomplete files
/api/agent/drops/[slug]/multipartMultipart upload operations (get part URL, list parts, complete)
/api/agent/drops/[slug]Get drop status and file list
/d/[slug]/downloadDirect file download (302 redirect)
Payment Flow
x402drop uses the x402 protocol (v2). No deposit or pre-authentication required. Your agent sends a request, receives a 402 challenge with USDC payment details, signs an EIP-3009 USDC transfer authorization, and retries with the signed payment.
POST /api/agent/drops← 402 PaymentRequiredSend file metadata + duration
Sign EIP-712 typed dataUSDC transferWithAuthorization
POST /api/agent/drops + Payment-Signature← 200 { slug, uploadUrls }On-chain settlement via facilitator
PUT files to presigned URLsDirect upload to R2 storage
POST .../complete← 200 { shareUrl }Files available for download
Step 1 — Create a Drop
Send a POST with file metadata and desired storage duration. The server computes the price and returns a 402 response.
Request
POST /api/agent/drops
Content-Type: application/json
{
"files": [
{ "name": "report.pdf", "sizeBytes": 1048576, "mimeType": "application/pdf" }
],
"durationSeconds": 3600
}402 Response
The 402 body includes standard x402 payment requirements plus an enriched pricing object.
{
"x402Version": 2,
"error": "PAYMENT_REQUIRED",
"resource": {
"url": "https://x402drop.com/api/agent/drops",
"description": "x402drop: store 1 file(s), 1.0 MB, 1 hours",
"mimeType": "application/json"
},
"accepts": [
{
"scheme": "exact",
"network": "eip155:8453",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"amount": "10000",
"payTo": "0x..."
},
{
"scheme": "exact",
"network": "eip155:42161",
"asset": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
"amount": "10000",
"payTo": "0x..."
}
],
"pricing": {
"priceUsd": "0.010000",
"durationSeconds": 3600,
"files": [{ "name": "report.pdf", "sizeBytes": 1048576 }]
}
}200 Response (after payment)
{
"slug": "abc123xyz",
"uploadToken": "a1b2c3d4...",
"txHash": "0x...",
"network": "base",
"expiresAt": "2026-03-04T12:00:00.000Z",
"shareUrl": "https://x402drop.com/d/abc123xyz",
"downloadUrl": "https://x402drop.com/d/abc123xyz/download",
"uploadUrls": [
{
"filename": "report.pdf",
"type": "single",
"url": "https://r2-presigned-upload-url...",
"r2Key": "drops/abc123xyz/1-report.pdf"
}
]
}Step 2 — Upload Files
Upload each file to its presigned URL using a standard HTTP PUT. For single-part uploads (files under 5 MB), just PUT the file body directly.
const fileBuffer = fs.readFileSync("report.pdf");
await fetch(uploadUrl, {
method: "PUT",
body: fileBuffer,
headers: { "Content-Type": "application/pdf" },
});Multipart Uploads (files ≥ 5 MB)
For files 5 MB or larger, the create-drop response returns type: "multipart" with an uploadId instead of a presigned URL. Use the multipart endpoint to get presigned URLs for each part, upload them, then complete the upload.
const PART_SIZE = 5 * 1024 * 1024; // 5 MB per part
const file = uploadEntry; // from create-drop response: { uploadId, r2Key }
const fileBuffer = fs.readFileSync("large-file.zip");
const totalParts = Math.ceil(fileBuffer.length / PART_SIZE);
const uploadedParts = [];
for (let i = 0; i < totalParts; i++) {
// Get presigned URL for this part
const partRes = await fetch(
`${API}/api/agent/drops/${slug}/multipart`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${uploadToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
action: "getPartUrl",
uploadId: file.uploadId,
r2Key: file.r2Key,
partNumber: i + 1,
}),
},
);
const { url } = await partRes.json();
// Upload this part
const start = i * PART_SIZE;
const end = Math.min(start + PART_SIZE, fileBuffer.length);
const partData = fileBuffer.slice(start, end);
const putRes = await fetch(url, { method: "PUT", body: partData });
const etag = putRes.headers.get("ETag");
uploadedParts.push({ PartNumber: i + 1, ETag: etag });
}
// Complete the multipart upload
await fetch(`${API}/api/agent/drops/${slug}/multipart`, {
method: "POST",
headers: {
"Authorization": `Bearer ${uploadToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
action: "complete",
uploadId: file.uploadId,
r2Key: file.r2Key,
parts: uploadedParts,
}),
});To resume an interrupted multipart upload, list already-uploaded parts first:
// List parts already uploaded
POST /api/agent/drops/{slug}/multipart
Authorization: Bearer <uploadToken>
{ "action": "listParts", "uploadId": "...", "r2Key": "..." }
// Response
{ "parts": [{ "PartNumber": 1, "Size": 5242880, "ETag": "..." }] }Step 3 — Mark Upload Complete
After uploading all files, call the complete endpoint with the upload token received from step 1. This makes the files available for download.
POST /api/agent/drops/{slug}/complete
Authorization: Bearer <uploadToken>
Content-Type: application/json
{} // empty body marks all files completeYou can optionally specify which files to mark complete:
{ "filenames": ["report.pdf"] }Resuming Failed Uploads
Presigned upload URLs expire after 1 hour. If an upload fails or gets interrupted, call the resume endpoint to get fresh URLs for any incomplete files. Uses the same upload token from step 1.
POST /api/agent/drops/{slug}/resume
Authorization: Bearer <uploadToken>
Content-Type: application/json
{}Response contains new presigned URLs only for files that haven’t been marked complete yet. If all files are already uploaded, it returns an empty array.
{
"uploadUrls": [
{
"filename": "report.pdf",
"sizeBytes": 1048576,
"type": "single",
"url": "https://r2-presigned-upload-url...",
"r2Key": "drops/abc123xyz/1-report.pdf"
}
]
}Step 4 — Download
The download URL returned in step 1 can be shared with other agents or humans. For agents, /d/{slug}/download returns a 302 redirect to the presigned R2 download URL (for single-file drops) or a JSON list of download URLs (for multi-file drops).
# Single-file drop — follows redirect, downloads file
curl -L https://x402drop.com/d/abc123xyz/download -o report.pdf
# Multi-file drop — get JSON list of download URLs
curl https://x402drop.com/d/abc123xyz/download
# Download a specific file from a multi-file drop
curl -L "https://x402drop.com/d/abc123xyz/download?file=report.pdf" -o report.pdfFull Code Example
Method 1: Native fetch + viem
No x402 packages required. Uses only viem for wallet signing and standard fetch.
import { privateKeyToAccount } from "viem/accounts";
import { toHex } from "viem";
const account = privateKeyToAccount("0xYOUR_PRIVATE_KEY");
const API = "https://x402drop.com";
// Step 1: Create drop — expect 402
const body = JSON.stringify({
files: [{ name: "data.json", sizeBytes: 2048, mimeType: "application/json" }],
durationSeconds: 3600,
});
const res402 = await fetch(`${API}/api/agent/drops`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body,
});
const challenge = await res402.json();
const accept = challenge.accepts.find((a) => a.network === "eip155:8453");
// Step 2: Sign EIP-3009 transferWithAuthorization
const nonce = toHex(crypto.getRandomValues(new Uint8Array(32)));
const validBefore = Math.floor(Date.now() / 1000) + 3600;
const signature = await account.signTypedData({
domain: {
name: "USD Coin",
version: "2",
chainId: 8453,
verifyingContract: accept.asset,
},
types: {
TransferWithAuthorization: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "validAfter", type: "uint256" },
{ name: "validBefore", type: "uint256" },
{ name: "nonce", type: "bytes32" },
],
},
primaryType: "TransferWithAuthorization",
message: {
from: account.address,
to: accept.payTo,
value: BigInt(accept.amount),
validAfter: 0n,
validBefore: BigInt(validBefore),
nonce,
},
});
// Step 3: Build payment payload and retry
const paymentPayload = {
x402Version: 2,
resource: challenge.resource,
accepted: accept,
payload: {
signature,
authorization: {
from: account.address,
to: accept.payTo,
value: accept.amount,
validAfter: "0",
validBefore: validBefore.toString(),
nonce,
},
},
};
const paymentHeader = btoa(JSON.stringify(paymentPayload));
const paidRes = await fetch(`${API}/api/agent/drops`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Payment-Signature": paymentHeader,
},
body,
});
const drop = await paidRes.json();
// drop = { slug, uploadToken, uploadUrls, shareUrl, downloadUrl, ... }
// Step 4: Upload file
await fetch(drop.uploadUrls[0].url, {
method: "PUT",
body: fileContent,
});
// Step 5: Mark complete
await fetch(`${API}/api/agent/drops/${drop.slug}/complete`, {
method: "POST",
headers: {
"Authorization": `Bearer ${drop.uploadToken}`,
"Content-Type": "application/json",
},
body: "{}",
});
console.log("Share URL:", drop.shareUrl);
console.log("Download URL:", drop.downloadUrl);Method 2: @x402/fetch (automatic)
The official x402 client library intercepts 402 responses, signs payment, and retries automatically.
npm install @x402/fetch @x402/evm viemimport { x402Client, wrapFetchWithPayment } from "@x402/fetch";
import { registerExactEvmScheme } from "@x402/evm/exact/client";
import { privateKeyToAccount } from "viem/accounts";
const client = new x402Client();
registerExactEvmScheme(client, {
signer: privateKeyToAccount("0xYOUR_PRIVATE_KEY"),
});
const fetchWithPayment = wrapFetchWithPayment(fetch, client);
const API = "https://x402drop.com";
// Automatic: sends request, handles 402, signs, retries
const res = await fetchWithPayment(`${API}/api/agent/drops`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
files: [{ name: "data.json", sizeBytes: 2048, mimeType: "application/json" }],
durationSeconds: 3600,
}),
});
const drop = await res.json();
// Upload files and complete as shown in Method 1, steps 4-5Error Responses
All error responses include a JSON body with error and retryable fields.
| Status | Meaning | Retryable |
|---|---|---|
400 | Invalid request body | No |
401 | Missing upload token | No |
402 | Payment required / failed | Check body |
403 | Wallet blocked / token invalid | No |
404 | Drop not found | No |
410 | Drop expired / deleted | No |