# For agents

This page is written for a model or an automated client that swaps on behalf of a user. It is
terse on purpose: every rule here is a contract, not advice.

## What the service does

Drip Tools sells testnet tokens for real mainnet tokens, one way only. You make **one payment** on
a mainnet FROM chain (ten EVM chains, Solana or TON) and the service pays the quoted testnet
amounts to up to 10 000 recipients on one TO chain (EVM, Solana, TON or Bitcoin testnets). Base URL
`https://drip.tools/api`, JSON over HTTPS, no auth, open CORS. Every swap is four steps:
**discover** (`GET /api/chains`) → **quote** (`POST /api/quote`) → **pay** (one transaction on the
FROM chain, exactly `fromTotal`) → **track** (`GET /api/order?from_tx_hash=…`). The payment is real
money and cannot be reversed; the price is the TO amount × the listed rate, converted at the live
Chainlink price of the FROM token, with no fee or spread. FROM-chain gas is yours.

## 1. Discover

`GET https://drip.tools/api/chains?unwrap=true` → a flat array of chain objects. The fields you use:

```json
{
  "chainId": "42161",
  "isTestnet": false,
  "isEnabled": true,
  "rpc": ["https://arbitrum.drpc.org", "https://arbitrum-one-rpc.publicnode.com"],
  "tokens": [
    { "symbol": "ETH", "address": "0x0000000000000000000000000000000000000000", "decimals": 18 },
    { "symbol": "USDC", "address": "0xaf88d065e77c8cc2239327c5edb3a432268e5831", "decimals": 6 }
  ],
  "contracts": { "depository": "0x52361c9412DceC6A83980fb065D0276ec001fACE" }
}
```

Optional fields: `isPaused` (present and `true` only while paused), `family` (`"solana"`, `"ton"`,
`"bitcoin"`; absent = EVM), `cluster` (Solana).

| Field | Rule |
| --- | --- |
| `chainId` | A **string** here. Solana FROM `1399811149`, TON FROM `1667471768`; EVM chains use their EVM id. |
| `isTestnet` | `false` = FROM side (you pay there). `true` = TO side (recipients receive there). |
| `isEnabled` / `isPaused` | Usable only when `isEnabled: true` and `isPaused` is absent or `false`. Otherwise skip the chain. |
| `family` | Absent = EVM. `"solana"`, `"ton"` or `"bitcoin"` otherwise. Selects the payment shape. |
| `rpc` | Ordered: `rpc[0]` primary, the rest fallbacks. |
| `tokens[]` | `address` `0x0000000000000000000000000000000000000000` = native coin on every chain; SPL tokens use the base58 mint. `decimals` converts amounts. Bitcoin testnet tokens add `quoteDecimals` and `minTo`. |
| `contracts.depository` | FROM chains only: the pay target (EVM contract, Solana / TON treasury). Absent → not payable. |

Optional before quoting: `GET /api/quotes?chainId=<toChainId>&symbol=<SYMBOL>` →
`{ "price": 0.25, "balance": "<base units>", "available": true }`. `available: false` or a
`balance` below your total means the quote will fail with `NO_TO_PRICE` / `NO_LIQUIDITY`.

## 2. Quote

`POST https://drip.tools/api/quote` with `Content-Type: application/json`. Every field is required:

```json
{
  "chainId": 8453,
  "fromToken": "0x0000000000000000000000000000000000000000",
  "sender": "0x9246ef7F2b7fEcE8AfD8a72271A76Cc2A8d8A3E5",
  "toChainId": 421614,
  "toToken": "0x0000000000000000000000000000000000000000",
  "recipients": ["0x2F4edA5e7EEa6eb634C7FBb0acAFA04b8938F63A"],
  "toAmounts": ["5000000000000000000"]
}
```

| Field | Rule |
| --- | --- |
| `chainId`, `toChainId` | **Numbers**. FROM must be an enabled mainnet, TO an enabled testnet. |
| `fromToken`, `toToken` | An `address` from that chain's `tokens[]`. |
| `sender` | The wallet that will sign and pay. EVM `0x…` any case; Solana base58; TON any form. |
| `recipients` | 1 to 10 000 addresses valid on the TO chain. |
| `toAmounts` | Base-unit **integer strings** of the TO token, one per recipient. No decimals, hex or exponents. |

Response `200` — the fields you must use:

```json
{
  "quoteId":   "0x1f5215a4577ef4548b550313d772594473e237c67514943751634818fdff9615",
  "vault":     "0x52361c9412dcec6a83980fb065d0276ec001face",
  "fromToken": "0x0000000000000000000000000000000000000000",
  "fromTotal": "517554316856097",
  "deadline":  1784121105,
  "signature": "0x23fbc0…1b",
  "signer":    "0x5C0917e114e49F3Cf266D6467ac129FEe42d77c5",
  "usdTotal":  "1.25"
}
```

| Field | Use |
| --- | --- |
| `quoteId` | The only reference carried on-chain: `pay` argument, Solana memo, or TON comment. |
| `vault` | Pay target, canonical form (EVM lowercased, Solana base58, TON `UQ…`). Equals `contracts.depository`. |
| `fromToken` | The token to pay with, canonical form. |
| `fromTotal` | The exact amount to pay, base units (wei, token units, lamports, nanotons). |
| `deadline` | Epoch seconds. The payment must land at or before it. Equals `expiry`. |
| `signature` | EIP-712 signature by `signer`. Verifying it is optional: [Verifying the quote signature](https://docs.drip.tools/swap/contract.md#verifying-the-quote-signature-optional). |

The response also carries `issuedAt`, `expiry`, `fromAmounts` and `toAmounts` (bookkeeping).

Errors are `{ "error": "<message>", "code": "<CODE>" }`. Decide by HTTP status and code:

| Response | Action |
| --- | --- |
| `400` + code | Fix the request; never resend it unchanged. `BELOW_MIN_TOTAL` raise amounts · `TO_TOO_PRECISE` / `TO_TOO_SMALL` / `BELOW_SOL_RENT_MIN` fix the amount · `NO_LIQUIDITY` lower amounts or pick another TO token · `BAD_TO_CHAIN` / `BAD_FROM_CHAIN` pick another chain · `TOO_MANY` split the batch. |
| `429` JSON `RATE_LIMITED` | Sleep `Retry-After` seconds, then retry. |
| `429` empty body | Edge flood guard: sleep a few seconds, then retry. |
| `503` `WARMING_UP` | Sleep `retryAfterSec`, then retry. |
| `503` `NO_FROM_PRICE` / `NO_TO_PRICE` / `SIGNING_DISABLED`, `500` `NO_VAULT` | Not now: another token or chain, or try much later. |

Every code: [Errors](https://docs.drip.tools/reference/errors.md).

## 3. Pay

One transaction on the FROM chain. Recipients and amounts are **not** in it; they live in the
signed quote. Pick the shape by the FROM chain's `family` and the token.

### EVM, native coin

```json
{
  "to": "<quote.vault>",
  "function": "pay(bytes32 quoteId, uint256 deadline)",
  "args": ["<quote.quoteId>", "<quote.deadline>"],
  "value": "<quote.fromTotal>"
}
```

### EVM, ERC-20 (approve, then pay)

```json
[
  { "to": "<quote.fromToken>", "function": "approve(address spender, uint256 amount)",
    "args": ["<quote.vault>", "<quote.fromTotal>"], "onlyIf": "allowance(sender, quote.vault) < fromTotal" },
  { "to": "<quote.vault>", "function": "payWithToken(address token, uint256 amount, bytes32 quoteId, uint256 deadline)",
    "args": ["<quote.fromToken>", "<quote.fromTotal>", "<quote.quoteId>", "<quote.deadline>"], "value": "0" }
]
```

Ethereum-mainnet USDT: if the allowance is non-zero but too small, `approve(vault, 0)` first.

### EVM, ERC-20 with EIP-2612 permit (one transaction)

```json
{
  "to": "<quote.vault>",
  "function": "payWithTokenPermit(address token, uint256 amount, bytes32 quoteId, uint256 deadline, uint256 permitDeadline, uint8 v, bytes32 r, bytes32 s)",
  "args": ["<quote.fromToken>", "<quote.fromTotal>", "<quote.quoteId>", "<quote.deadline>", "<permitDeadline>", "<v>", "<r>", "<s>"],
  "permit": { "owner": "<sender>", "spender": "<quote.vault>", "value": "<quote.fromTotal>" }
}
```

Only for tokens that implement `permit` (native USDC does, Ethereum USDT does not). Full example:
[Examples → ERC-20 with permit](https://docs.drip.tools/swap/examples.md#erc-20-with-permit).

ABI for all EVM variants:

```js
const depositoryAbi = parseAbi([
  'function pay(bytes32 quoteId, uint256 deadline) payable',
  'function payWithToken(address token, uint256 amount, bytes32 quoteId, uint256 deadline)',
  'function payWithTokenPermit(address token, uint256 amount, bytes32 quoteId, uint256 deadline, uint256 permitDeadline, uint8 v, bytes32 r, bytes32 s)',
])
```

Reverts: `Expired` (after `deadline`), `MissingQuote`, `AmountTooLow` / `ZeroAmount`,
`TokenTransferMismatch`, `TokenNotAllowed`, `DirectPaymentNotAllowed` (plain transfer, no call),
`EnforcedPause`. A reverted transaction pays nothing (gas aside) and does not consume the quote.

### Solana (`family: "solana"`)

```json
{
  "feePayer": "<sender>",
  "instructions": [
    "SPL only: ATA CreateIdempotent(payer = sender, owner = quote.vault, mint = quote.fromToken)",
    "native SOL: SystemProgram.transfer(from = sender, to = quote.vault, lamports = quote.fromTotal)",
    "SPL token:  TransferChecked(sender ATA -> quote.vault ATA, mint = quote.fromToken, amount = quote.fromTotal, decimals)",
    "Memo(program MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr, data = UTF-8 quote.quoteId, no signer accounts)"
  ],
  "trackRef": "<transaction signature>"
}
```

SPL tokens use the classic Token program. Keep about 0.003 SOL for the fee and the treasury's
token-account rent. Payments are indexed at `finalized`, typically 15–30 s after confirmation.

### TON (`family: "ton"`)

```json
{
  "from": "<sender wallet>",
  "message": {
    "to": "<quote.vault, the UQ… address as returned>",
    "value": "<quote.fromTotal> nanotons",
    "bounce": false,
    "body": "text comment = <quote.quoteId> (op 0 + UTF-8, one cell)"
  },
  "sendMode": 3,
  "validUntil": "<quote.deadline>",
  "trackRef": "<quote.quoteId>"
}
```

Mode 3 = `PAY_GAS_SEPARATELY | IGNORE_ERRORS` (mode 1 also works). Mode 0 takes the forward fee
out of the value, the treasury receives less than `fromTotal`, and the payment is held for review.
`@ton/ton` does not set it for you: pass `sendMode` explicitly. One mnemonic yields a different address
per wallet version (W5 / v4r2); quote the address you actually pay from. Payments are indexed
10–20 s after they land. Full shape and rules: [Paying from Solana & TON](https://docs.drip.tools/swap/memo.md).

## 4. Track

`GET https://drip.tools/api/order?from_tx_hash=<ref>`, where `ref` is the EVM transaction hash, the
Solana transaction signature, or the **`quoteId`** on TON.

```json
{
  "data": [
    {
      "status": "completed",
      "id": "abc123…",
      "from": { "chainId": "8453", "symbol": "ETH", "amount": "0.00051755", "address": "0x<payer>", "txHash": "0x<payment>" },
      "to": { "chainId": "421614", "symbol": "ETH", "amount": "5.00000000", "address": "0x<recipient>", "txHash": "0x<payout>", "ts": 1784120990 }
    }
  ],
  "refund": null
}
```

| Field | Meaning |
| --- | --- |
| `data` | One order per recipient; `[]` until the payment is indexed. |
| `status` | `processing` → `completed` \| `failed`. `pending` is reserved and never produced. |
| `to.txHash` | The testnet payout transaction; `null` until dispensed. Proof of delivery. |
| `refund` | `null`, or `{ "status": "refund_pending" \| "refunded", "reason", "from", "refundTxHash", "refundedAt" }`. |

## Invariants

> **Warning: Real money**
>
> A payment cannot be cancelled. A payment that breaks invariants 1–4 does not become an order and
> is not refunded automatically.

1. Pay exactly `fromTotal` base units of `quote.fromToken` to `quote.vault`, from the wallet you quoted as `sender`, before `deadline`; never round, pad or convert the amount.
2. One payment carries exactly one `quoteId`, and one `quoteId` is paid by exactly one payment.
3. Never pay a quote twice: once a payment transaction is broadcast, only track it, even if the receipt wait failed or timed out.
4. A payment with the wrong payer, token or amount, or a second payment of an honoured quote, is held for operator review with no automatic refund; recover it only via [support](https://docs.drip.tools/reference/errors.md#payments-held-for-review) with the transaction hash.
5. A quote is free and non-binding and expires 180 s after issue, so request it immediately before paying and request a new one if fewer than 20 s of it remain.
6. A late payment reverts with `Expired` on EVM (nothing paid) and, on Solana / TON, lands and is refunded with reason `expired` instead of becoming an order.
7. The minimum order is $1 for one recipient and $5 for a multisend (more than one recipient), else `BELOW_MIN_TOTAL`.
8. One quote holds up to 10 000 recipients paid by one transaction; split larger batches into several quotes.
9. TO amounts sit on a 4-decimal grid of the TO token (whole sats on Bitcoin), and every recipient on a Solana TO chain gets at least 0.001 SOL.
10. Skip chains with `isEnabled: false` or `isPaused: true`, and FROM chains without `contracts.depository`.
11. Rate limits are 600 requests / min per IP on every route, 10 quotes / min per IP and 30 quotes / min per `sender`, and a `429` answers `{ "error": "RATE_LIMITED", "retryAfterSec": n }` with a `Retry-After` header to sleep on.
12. `chainId` is a string in `/api/chains` and in order objects but a number in `POST /api/quote`, so coerce before comparing.
13. EVM addresses compare case-insensitively (quotes return lowercase, orders checksummed), Solana base58 keys are case-sensitive and never re-cased, and a TON payer is normalized to the non-bounceable `UQ…` form.
14. Quote amounts are base-unit integer strings while order amounts are human decimals with varying trailing zeros, so parse them as decimals and never compare them as strings.
15. Read the pay target at runtime (`contracts.depository` or `quote.vault`) and never send funds to it without a quote.

To build a TO amount on the grid: `BigInt(Math.round(x * 1e4)) * 10n ** BigInt(decimals - 4)` for
a token with 4 or more decimals.

## When to stop polling

- Poll every 5 s. EVM payouts land a median 3 s after the payment is indexed; Solana adds 15–30 s
  of finality, TON 10–20 s.
- **Stop** when `data.length > 0` and every order is `completed` or `failed`. A `failed` order's
  FROM slice is refunded (`reason: "failed_order"`, recorded within a few minutes; for a partial
  batch capped so the delivered part still cost at least $1).
- **Stop** when `data` is empty and `refund` is set: the payment was refused (`expired`,
  `unknown_quote`, `unattributed`, `ambiguous_memo`, `bad_signature`) and no order will appear.
- **Stop** when the EVM receipt says `reverted`: nothing was paid; quote again if needed.
- **Stop and report** when `data` and `refund` stay empty well past the deadline (10 minutes is
  generous): the payment was most likely held for review (invariant 4).
- TON: `from_tx_hash=<quoteId>` resolves only once the payment is honoured (afterwards the order's
  `from.txHash`, 64 hex characters, works too). A refused TON payment never resolves by `quoteId`;
  find its refund in `GET /api/orders?address=<UQ…>&view=batches` (rows with `isRefund: true`).
  `GET /api/orders/load` does not resolve a `quoteId`; pass `data[0].id`.
- Large batches: `&paginate=1&limit=0` returns only
  `summary: { recipients, statusCounts, fromTotal, toTotal, completedTs, lastPayout }`; stop when
  `summary.recipients > 0` and `summary.completedTs` is non-null.
- Many payments: `POST /api/orders/status` with `{ "ids": [...] }` (500 ids at most) instead of one
  lookup per payment. Order ids are case-sensitive.
- Optional: `GET /api/orders/load?ref=<txHash|orderId>` for a queue ETA, and
  `GET /api/orders/stream?address=<payer>` (Server-Sent Events, not rate-limited) whose
  `orders-changed` event lists FROM tx hashes to re-fetch. If the stream answers `503`, poll.

Details: [Orders](https://docs.drip.tools/api/orders.md), [Order lifecycle & refunds](https://docs.drip.tools/reference/lifecycle.md).

## Machine-readable resources

| Resource | Content |
| --- | --- |
| [`llms.txt`](https://docs.drip.tools/llms.txt) | Index of every page, one line each. |
| [`llms-full.txt`](https://docs.drip.tools/llms-full.txt) | Every page in one markdown file. |
| [`openapi.json`](https://docs.drip.tools/openapi.json) | The whole API as OpenAPI 3.1. |
| Any page + `.md` | The page as markdown, e.g. `https://docs.drip.tools/guide/agents.md`. |
| [Examples](https://docs.drip.tools/swap/examples.md) | Runnable JavaScript and Python scripts for every payment shape; `DRY_RUN=1` requests a real quote and builds the payment but sends nothing. |

## Recipe: one swap, EVM native coin

JavaScript with `viem`. Pays ETH on Base, delivers 5 testnet ETH on Arbitrum Sepolia. Replace the
four constants; everything else is the contract above.

```js
import { createPublicClient, createWalletClient, http, fallback, parseAbi } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'

const API = 'https://drip.tools/api'
const NATIVE = '0x0000000000000000000000000000000000000000'
const FROM_CHAIN_ID = 8453                       // Base (number in POST /quote)
const TO_CHAIN_ID = 421614                       // Arbitrum Sepolia
const RECIPIENT = '0x…'                          // the user's testnet wallet
const TO_AMOUNT = '5000000000000000000'          // 5 ETH in wei, on the 4-decimal grid
const account = privateKeyToAccount(process.env.PRIVATE_KEY)   // the payer = `sender`
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))

async function api(path, init) {
  for (let attempt = 0; attempt < 5; attempt++) {
    const res = await fetch(`${API}${path}`, init)
    const body = await res.json().catch(() => null)            // the edge 429 has no JSON body
    if (res.status === 429 || body?.error === 'WARMING_UP') {
      await sleep(1000 * Number(res.headers.get('retry-after') ?? 2))
      continue
    }
    if (!res.ok) throw new Error(`${res.status} ${body?.code ?? body?.error}`)
    return body
  }
  throw new Error('still rate limited')
}

// 1. Discover
const chains = await api('/chains?unwrap=true')
const from = chains.find((c) => c.chainId === String(FROM_CHAIN_ID))
const to = chains.find((c) => c.chainId === String(TO_CHAIN_ID))
if (!from?.contracts?.depository || !from.isEnabled || from.isPaused) throw new Error('FROM chain not payable')
if (!to?.isTestnet || !to.isEnabled || to.isPaused) throw new Error('TO chain not available')

// 2. Quote, right before paying
const quote = await api('/quote', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    chainId: FROM_CHAIN_ID, fromToken: NATIVE, sender: account.address,
    toChainId: TO_CHAIN_ID, toToken: NATIVE, recipients: [RECIPIENT], toAmounts: [TO_AMOUNT],
  }),
})
if (quote.deadline - Date.now() / 1000 < 20) throw new Error('quote nearly expired: quote again')

// 3. Pay once, exactly fromTotal
const transport = fallback(from.rpc.map((url) => http(url)))  // rpc[0] first
const wallet = createWalletClient({ account, transport })
const hash = await wallet.writeContract({
  address: quote.vault,
  abi: parseAbi(['function pay(bytes32 quoteId, uint256 deadline) payable']),
  functionName: 'pay',
  args: [quote.quoteId, BigInt(quote.deadline)],
  value: BigInt(quote.fromTotal),
  chain: null,
})
// From here on this quote is spent: never pay it again, only track `hash`.
const receipt = await createPublicClient({ transport })
  .waitForTransactionReceipt({ hash }).catch(() => null)     // optional; the API is the source of truth
if (receipt?.status === 'reverted') throw new Error('reverted: nothing was paid')

// 4. Track until every order is terminal
const terminal = (o) => o.status === 'completed' || o.status === 'failed'
const stopAt = Date.now() + 10 * 60_000                        // then report: likely held for review
while (Date.now() < stopAt) {
  const { data, refund } = await api(`/order?from_tx_hash=${hash}`)
  if (data.length && data.every(terminal)) {
    for (const o of data) console.log(o.to.address, o.status, o.to.txHash)
    if (refund) console.log('refund', refund.status, refund.reason)   // failed slices
    break
  }
  if (!data.length && refund) { console.log('refused', refund.reason); break }
  await sleep(5000)
}
```

For ERC-20, Solana and TON, swap step 3 for the matching shape above; the other steps are identical
except the track `ref` (Solana signature, TON `quoteId`).
