Using Payday

Withdrawals

Deposits settle to the Payday wallet on whichever network the payer chose. A withdrawal moves everything it holds, on every network, to one address you name, with nothing deducted. Payday relays and pays the gas; your signature decides where each leg's funds may land.

How a withdrawal works#

The Payday wallet is an ordinary account with the same address on every network, and its key is yours: Privy holds it for you, Payday never has it. So Payday cannot simply move your funds, and by design it cannot. Instead a withdrawal is four steps, the same over the API as in the dashboard:

  1. Prepare

    POST /v1/withdrawals reads the wallet's USDC on every network and answers with one leg per network that holds any. A leg is a transfer when the funds already sit on the destination network, and a bridge when they cross through Circle's CCTP. Each leg carries the EIP-712 document to sign.
  2. Sign

    The document is an EIP-3009 authorization under that network's USDC contract: TransferWithAuthorization naming your destination as the payee, or ReceiveWithAuthorization naming Payday's WithdrawalForwarder as the payee with a nonce that commits to your destination. The dashboard signs with the wallet Privy holds; a server signs with the exported key.
  3. Submit

    POST /v1/withdrawals/{id}/authorizations takes the signatures. Payday verifies each against your wallet and hands the legs to its relayer.
  4. Poll

    GET /v1/withdrawals/{id} follows every leg to the destination: the transfer, or the burn, Circle's attestation, and the mint.

Timing and cost#

LegWhat happensTypical time
TransferOne transferWithAuthorization on the destination network.Seconds
Bridge from MonadBurn, Circle attests once the block is final, mint on the destination.Under a minute
Bridge from Base or ArbitrumSame, but Circle waits for Ethereum finality (~65 blocks) before attesting.15–20 minutes

You receive the full amount: CCTP's standard transfer has no protocol fee, and Payday pays every transaction's gas. One withdrawal may be open per account; its authorizations are valid for 24 hours, and a leg left unsigned that long expires.

From the dashboard#

The Account section shows the wallet's balance on each network and a Withdraw control beneath them. Pick the destination network, enter the address, review the legs, and sign once per network. The page checks each document against its leg before it asks the wallet, then tracks every leg until it lands; you can leave while a bridge waits for Circle.

From your server#

A server needs the wallet's key. Export it once from the Account section (Export wallet key): Privy shows it to you on its own origin, and Payday never sees it. Anyone holding it controls the wallet, so keep it in a secret manager next to your API key and rotate the API key if either leaks.

.env
PAYDAY_WALLET_KEY=0x…   # exported once from the dashboard; store it in a secret manager

Then the four steps, in the official SDK or in any language with an HTTP client and an EIP-712 library. Every uint256 in the document is a decimal string, the nonce is 0x hex, and the signature is 65 bytes r || s || v with v 27 or 28, sent as 0x hex.

TypeScript
import { PaydayClient } from "@payday/sdk";
import { privateKeySigner, signWithdrawal } from "@payday/sdk/signing"; // needs viem installed

const payday = new PaydayClient({ apiKey: process.env.PAYDAY_API_KEY! });
// The Payday wallet key, exported once from the dashboard's Account section.
const signer = await privateKeySigner(process.env.PAYDAY_WALLET_KEY!);

// 1. Prepare: one leg per network the wallet holds USDC on.
let withdrawal = await payday.withdrawals.create(
  { destination: { chain_id: "8453", address: "0x1111111111111111111111111111111111111111" } },
  crypto.randomUUID(),
);

// 2. Sign: checks every document against its leg, then signs it.
const chains = (id: number) => deploymentChains.get(id) ?? null; // trusted USDC/CCTP registry
const { authorizations } = await signWithdrawal(withdrawal, signer, { chains });

// 3. Submit, then 4. poll until every leg has landed.
withdrawal = await payday.withdrawals.authorize(withdrawal.id, authorizations);
while (withdrawal.status === "in_progress") {
  await new Promise((resolve) => setTimeout(resolve, 10_000));
  withdrawal = await payday.withdrawals.get(withdrawal.id);
}
console.log(withdrawal.status, withdrawal.legs.map((leg) => [leg.state, leg.mint_tx_hash ?? leg.transfer_tx_hash]));

Another language? Hand your agent the whole specification, examples included, and let it build the same four steps there.

What to check before signing#

The document says where funds go, so a signer should trust the leg, not the document. The SDK's signWithdrawal and the dashboard both refuse a document that fails any of these; do the same:

  • typed_data.message.from is your Payday wallet (the withdrawal's wallet_address).
  • typed_data.domain.chainId is the leg's source_chain.id, and verifyingContract is that chain's native USDC.
  • typed_data.message.value equals the leg's amount_base_units, and validAfter is "0".
  • typed_data.message.validBefore is still in the future.
  • For a transfer leg, typed_data.message.to is your destination address.
  • For a bridge leg, typed_data.message.to and authorization.forwarder equal the trusted WithdrawalForwarder for the source chain; nonce_preimage.destination_domain equals the trusted destination chain's CCTP domain; nonce_preimage.mint_recipient is your destination address; and typed_data.message.nonce equals keccak256(abi.encode(uint32 destination_domain, bytes32(mint_recipient), bytes32 salt)) over nonce_preimage.

The forwarder on each network:

NetworkCCTP domainWithdrawalForwarder
This deployment bridges nothing; withdrawals stay on their network.

Test vector#

A bridge leg exactly as the API returns it. Its document hashes to the EIP-712 digest 0xfe0bcc7d9e69ee02881011f29a4156caa15e02b8e94c2e0c9f40e66711651993, and its nonce 0x18b79105e486e10f626b71939a0226c47316949b7c24ff1c397661ea861fafaa is keccak256(abi.encode(uint32 6, bytes32(0x…d00d), bytes32(0x…07))) over the preimage. Payday's Rust, Solidity, TypeScript and browser code all pin these two values; an implementation that reproduces them signs what Payday expects.

A bridge leg
{
  "id": "wdl_0198f80c-8d2f-7dc1-a369-90556a64f700",
  "kind": "bridge",
  "source_chain": { "id": "143", "name": "Monad", "native_symbol": "MON" },
  "amount": "1.234567",
  "amount_base_units": "1234567",
  "state": "awaiting_signature",
  "authorization": {
    "primary_type": "ReceiveWithAuthorization",
    "typed_data": {
      "domain": {
        "name": "USDC",
        "version": "2",
        "chainId": 143,
        "verifyingContract": "0x754704Bc059F8C67012fEd69BC8A327a5aafb603"
      },
      "primaryType": "ReceiveWithAuthorization",
      "types": {
        "EIP712Domain": [
          { "name": "name", "type": "string" },
          { "name": "version", "type": "string" },
          { "name": "chainId", "type": "uint256" },
          { "name": "verifyingContract", "type": "address" }
        ],
        "ReceiveWithAuthorization": [
          { "name": "from", "type": "address" },
          { "name": "to", "type": "address" },
          { "name": "value", "type": "uint256" },
          { "name": "validAfter", "type": "uint256" },
          { "name": "validBefore", "type": "uint256" },
          { "name": "nonce", "type": "bytes32" }
        ]
      },
      "message": {
        "from": "0x1111111111111111111111111111111111111111",
        "to": "0x2222222222222222222222222222222222222222",
        "value": "1234567",
        "validAfter": "0",
        "validBefore": "1800000000",
        "nonce": "0x18b79105e486e10f626b71939a0226c47316949b7c24ff1c397661ea861fafaa"
      }
    },
    "expires_at": "2027-01-15T08:00:00Z",
    "forwarder": "0x2222222222222222222222222222222222222222",
    "nonce_preimage": {
      "destination_domain": 6,
      "mint_recipient": "0x000000000000000000000000000000000000d00d",
      "salt": "0x0000000000000000000000000000000000000000000000000000000000000007"
    }
  },
  "transfer_tx_hash": null,
  "burn_tx_hash": null,
  "mint_tx_hash": null,
  "failure_reason": null
}

States#

Leg stateMeaning
awaiting_signaturePrepared; authorization is present.
authorizedSigned and queued for the relayer.
relayingThe transfer or burn is in flight.
burned, attested, mintingBridge legs only: burned and waiting for Circle; attested and queued to mint; mint in flight.
completedLanded. transfer_tx_hash or burn_tx_hash + mint_tx_hash name the transactions.
failed, expired, cancelledTerminal without landing; failure_reason says why. Funds stay where the last successful step left them.

The withdrawal itself is awaiting_signature while any leg needs a signature, in_progress while the relayer works, then completed, failed or cancelled. The API reference starts at Create a withdrawal.