Docs
Rill is continuous USDC payment streams on Arc. A sender locks USDC that vests to a recipient linearly, per second, via the on-chain StreamPay contract.
Overview
Three actions make up the whole protocol:
- Create a stream: recipient, amount, and a start/stop window. The deposit is pulled into the contract.
- Withdraw: the recipient pulls whatever has vested so far, at any time.
- Cancel: either party ends it. Vested-but-unwithdrawn funds go to the recipient, the rest returns to the sender.
Network
| Network | Arc Testnet |
| Chain ID | 5042002 |
| RPC | https://rpc.testnet.arc.network |
| StreamPay | 0xd981229808c89e1689e025E7c5367d1154F1899D |
| USDC (ERC-20, 6 decimals) | 0x3600000000000000000000000000000000000000 |
USDC is Arc's native asset; amounts are in base units (6 decimals), so 1000000 is 1 USDC.
Integrate
The stream helpers live in this repo at site/lib/stream(network config, the StreamPay ABI, and typed client + math functions). Copy that folder into your app, or call the contract directly with viem and the address below. Set up clients against Arc:
import { createPublicClient, createWalletClient, http } from "viem";
import { arcTestnet, defineArcChain } from "@/lib/stream";
const chain = defineArcChain(arcTestnet);
const publicClient = createPublicClient({ chain, transport: http(arcTestnet.rpcUrl) });
const walletClient = createWalletClient({ chain, transport: http(arcTestnet.rpcUrl), account });Create a stream
Approve (handled for you) and open a stream. Returns the new stream id.
import { createStream, arcTestnet } from "@/lib/stream";
const now = Math.floor(Date.now() / 1000);
const { streamId } = await createStream(
{ net: arcTestnet, publicClient, walletClient, account },
{
recipient: "0x…",
deposit: 10_000_000n, // 10 USDC
startTime: now,
stopTime: now + 3600, // over 1 hour
},
);Withdraw & cancel
import { withdraw, cancelStream } from "@/lib/stream";
// recipient pulls a specific amount of vested USDC
await withdraw({ net: arcTestnet, publicClient, walletClient, account }, streamId, 500_000n);
// either party ends the stream and splits the balance
await cancelStream({ net: arcTestnet, publicClient, walletClient, account }, streamId);Stream math
The SDK mirrors the contract exactly, so a UI can tick the streamed value locally without an RPC call per frame.
import { readStream, streamedAt, withdrawableAt } from "@/lib/stream";
const stream = await readStream({ net: arcTestnet, publicClient }, streamId);
const now = Date.now() / 1000;
streamedAt(stream, now); // total vested at 'now'
withdrawableAt(stream, now); // vested minus already withdrawnReady to try it? Open a stream on the app.