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.

Quick start: Install @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

plain
https://x402drop.com

Endpoints

POST
/api/agent/drops

Create a drop (x402 payment)

POST
/api/agent/drops/[slug]/complete

Mark file uploads as complete

POST
/api/agent/drops/[slug]/resume

Get fresh upload URLs for incomplete files

POST
/api/agent/drops/[slug]/multipart

Multipart upload operations (get part URL, list parts, complete)

GET
/api/agent/drops/[slug]

Get drop status and file list

GET
/d/[slug]/download

Direct 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.

x402 Payment & Upload Flow
1
POST /api/agent/drops
← 402 PaymentRequired

Send file metadata + duration

2
Sign EIP-712 typed data

USDC transferWithAuthorization

3
POST /api/agent/drops + Payment-Signature
← 200 { slug, uploadUrls }

On-chain settlement via facilitator

4
PUT files to presigned URLs

Direct upload to R2 storage

5
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

json
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.

json
{
  "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)

json
{
  "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.

javascript
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.

javascript
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:

json
// 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.

json
POST /api/agent/drops/{slug}/complete
Authorization: Bearer <uploadToken>
Content-Type: application/json

{}  // empty body marks all files complete

You can optionally specify which files to mark complete:

json
{ "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.

json
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.

json
{
  "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).

bash
# 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.pdf

Full Code Example

Method 1: Native fetch + viem

No x402 packages required. Uses only viem for wallet signing and standard fetch.

javascript
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.

bash
npm install @x402/fetch @x402/evm viem
javascript
import { 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-5

Error Responses

All error responses include a JSON body with error and retryable fields.

StatusMeaningRetryable
400Invalid request bodyNo
401Missing upload tokenNo
402Payment required / failedCheck body
403Wallet blocked / token invalidNo
404Drop not foundNo
410Drop expired / deletedNo