TESTNET: this deployment settles on Base Sepolia (chain 84532) and Arc Testnet (chain 5042002), both with test USDC, and can deliver a payment to any chain Circle CCTP supports in the same environment. The chain your key settles on is carried in the key itself. Do not treat testnet balances or transactions as real payments.
DEVELOPER DOCS

Agent API

Agents integrate with Treasury Copilot through HTTP only. The platform signs GenLayer transactions, enforces per-agent policy bindings, executes approved requests through 1Shot, and returns JSON. No wallet library, gas management, or GenLayer SDK is required.

Base URL: https://treasurycopilot.app. All agent paths are under /api/v1. Human-facing setup and policy pages are separate and require wallet authentication.

Machine-readable contract: OpenAPI 3.1 JSON

Authentication

Send the bearer API key issued during owner setup. The key is a stateless signed token encoding owner, agent, policy, chain, token, decimals, and version. Every request validates the key, the claimed agent_address, registry binding, and policy state.

Authorization: Bearer tcp_***

The MetaMask permission does not create the key. A fresh unique key is issued only after the GenLayer policy and delegation are finalized, registered, and read back successfully. Keys are shown once and expire after 30 days. Store them securely. Rotation increments the on-chain key version and invalidates older keys; revocation deactivates the policy binding.

Quickstart

  1. 1. Store the one-time API key in a secret manager or environment variable.
  2. 2. Call GET /balance. Check ready_to_spend and read blockers. This is the source of truth for your budget: it returns weekly_cap, weekly_available and per_tx_cap. An amount exactly equal to the per-transaction cap is allowed.
  3. 3. Obtain your recipient from the owner, out of band. Agent keys cannot list the approved recipients, deliberately: an agent that could enumerate them could also probe the policy. If you do not know your approved recipient, submit and read denial_code — recipient_not_approved means you need the owner to whitelist it.
  4. 4. Submit a decimal string amount, a stable idempotency key, and evidence.
  5. 5. 202 is not approval. It means queued. Keep polling poll_url until the request reaches a terminal state: executed or denied. A naive integration that treats 202 as success will report payments that never happened.

The key identifies the agent but never signs a blockchain transaction. Every GenLayer write is signed by the server platform wallet after the key, registry, policy, funding account, chain, token, and execution reporter are verified.

Merchant names are not recipient proof. A merchant-specific policy needs an owner-approved recipient or independently verified invoice evidence; otherwise requests fail closed.

POST /api/v1/spend

Validates the request, verifies evidence, submits it to GenLayer, and returns 202 Accepted immediately. The worker reviews finalized queued requests with prompt-comparative consensus and executes approved requests through 1Shot.

curl -X POST https://treasurycopilot.app/api/v1/spend \
  -H "Authorization: Bearer ***" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_address": "0xYourRegisteredAgent",
    "recipient": "0xabc...",
    "amount": "25.00",
    "category": "api_subscription",
    "justification": "Monthly API renewal, invoice #4471",
    "idempotency_key": "billing-4471-2026-07",
    "evidence": [{
      "type": "signed_invoice",
      "invoice_id": "4471",
      "merchant_id": "vercel",
      "expected_recipient": "0xabc...",
      "expected_amount": "25000000",
      "issued_at": 1784800000,
      "expires_at": 1785400000,
      "content_hash": "0x...",
      "signer": "0xMerchantSigner",
      "signature": "0x..."
    }]
  }'

Request body

{
  "agent_address": "0xYourRegisteredAgent",
  "recipient": "0xRecipient",
  "amount": "25.00",
  "category": "api_subscription",
  "justification": "Monthly API renewal invoice #4471",
  "idempotency_key": "billing-4471-2026-07",
  "destination_chain": 84532,
  "evidence": [
    {
      "type": "invoice_url",
      "invoice_id": "inv-4471",
      "merchant_id": "example-observability",
      "expected_recipient": "0xRecipient",
      "expected_amount": "25000000",
      "issued_at": 1791441323,
      "uri": "https://billing.example.com/invoices/4471",
      "merchant_domain": "billing.example.com",
      "sha256": "0xTHE_SHA256_OF_THE_BYTES_THAT_URL_SERVES"
    }
  ]
}

Evidence is required: 1 to 3 items. An empty array is rejected with 422 invalid_evidence. An invoice_url must be HTTPS, must return HTTP 200, and must serve an accepted content type —application/json and application/pdf work; an image/* response does not. The sha256 must be the digest of the bytes the URL actually serves, so it changes whenever the document does.

destination_chain is optional. Omit it to settle on your treasury's own chain. Supply any chain Circle supports and the payment is bridged there over CCTP. An unsupported chain id is refused immediately, and one idempotency_key cannot be replayed with a different destination.

Response

{
  "request_id": "0x...",
  "verdict": "pending",
  "reasoning": "Submitted to GenLayer and awaiting finalized policy review",
  "status": "submitted",
  "poll_url": "/api/v1/requests/0x...",
  "request": {
    "status": "submitted",
    "recipient": "0x...",
    "amount": "25",
    "amount_units": "25000000",
    "decision_mode": "prompt_comparative",
    "execution_status": "submitted",
    "tx_hash": "",
    "explorer_url": null,
    "created_at": "2026-07-21T12:10:00.000Z",
    "updated_at": "2026-07-21T12:11:00.000Z",
    "route": "direct",
    "destination_chain": { "chain_id": 84532, "name": "Base Sepolia" },
    "bridge": {
      "tx_hash": "",
      "attestation_status": "",
      "destination_tx_hash": "",
      "destination_explorer_url": null
    }
  },
  "chain": {
    "chain_id": 84532,
    "name": "Base Sepolia",
    "explorer_url": "https://sepolia.basescan.org"
  },
  "idempotent_replay": false,
  "genlayer": {
    "request_tx_hash": "0x..."
  }
}

The response includes Location and Retry-After: 10. Poll until the request reaches denied, executed, or a retryable failed state.

Networks and settlement

A treasury settles one of two ways, decided by the chain it is bound to. You never choose the mechanism; the chain does.

ChainHow it settlesWhat you hold
Base SepoliaERC-7715 permission relayed by 1ShotUSDC in your own smart account
Arc TestnetAn agent treasury vault you deploy and ownUSDC in the vault; you keep withdraw and pause

Arc has neither the MetaMask Delegation Framework nor 1Shot support, so there is no wallet permission to sign there. Instead you deploy a vault and name the platform executor on it. The executor can only pay out a request GenLayer has already approved, and it can never withdraw.

Paying on another network

Fund your treasury on whichever network suits you. Your agent then asks to be paid on whichever network suits it, per request, and Circle CCTP moves the money. A new policy permits every chain in its own Circle environment, so you do not keep a list.

Testnet destinationChain idCCTP domain
Ethereum Sepolia111551110
OP Sepolia111554202
Arbitrum Sepolia4216143
Base Sepolia845326
Polygon Amoy800027
Arc Testnet504200226

A testnet shares its mainnet CCTP domain, so a domain alone does not identify a chain. Mixing a testnet and a mainnet endpoint is refused outright rather than attempted, because Circle would never attest such a transfer. CCTP deducts its fee from the amount it moves, so the treasury burns the approved amount plus the fee and the payee receives exactly what the policy approved.

POST /api/owner/fund-arc

Owner session required. Returns the two transactions you sign in your own wallet to move USDC from any supported chain into your vault: an approval and a CCTP burn whose mint recipient is the vault. The platform never holds authority over your source-chain USDC.

{
  "source_chain": 11155111,
  "vault": "0xYourVault",
  "amount": "5000000"
}

amount is in USDC base units and is what the vault receives. The response adds the CCTP fee on top as total_amount_units, which is what you actually burn.

GET /api/v1/balance

Returns live token balance, weekly spent, weekly cap, and per-request cap. Values include display decimals and raw integer units.

curl -X GET https://treasurycopilot.app/api/v1/balance \
  -H "Authorization: Bearer ***"

GET /api/v1/history

Returns on-chain request history from GenLayer for the current API key binding. Includes status, verdict, reasoning, and execution hash when available.

curl -X GET "https://treasurycopilot.app/api/v1/history?limit=50" \
  -H "Authorization: Bearer ***"

Merchant identity and policy safety

Category and justification are untrusted claims. Policy V5 accepts up to three verified evidence items: an HTTPS invoice whose fetched bytes match a SHA-256 digest, or an EIP-712 signed invoice bound to the exact policy, chain, token, recipient, amount, timestamps, and content hash.

Fast approval has been removed. Every valid V5 request begins GenLayer prompt-comparative review in the original submission transaction. V4 remains supported through automatic recovery; V2 and V3 policies are blocked.

GET /api/v1/requests/:id

Returns one request record by ID, scoped to the authenticated API key. Use this endpoint for status polling or to recover the explorer link after submission.

curl -X GET https://treasurycopilot.app/api/v1/requests/0xREQUEST_ID \
  -H "Authorization: Bearer ***"

Recover by idempotency key

curl -X GET "https://treasurycopilot.app/api/v1/requests?idempotency_key=billing-4471-2026-07" \
  -H "Authorization: Bearer ***"

Addresses are compared case-insensitively, so EIP-55 checksummed and all-lowercase forms are equivalent.

GET /api/v1/policy returns only what an agent needs to form a valid request: where it settles, which token and precision to use, and whether its settlement binding is registered. Caps, remaining weekly budget, policy text and the recipient whitelist are deliberately withheld from agent keys, because an agent that can read them can size a request to sit just under a cap and mirror the policy wording back as justification. A denial always states its reason. Raw delegation payloads, permission contexts, signatures and signer secrets are never returned.

Lifecycle and polling

submitted

The platform signer submitted the policy transaction; the API returned 202.

reviewing

V5 comparative consensus is running in the submission transaction.

review_pending

A legacy V4 queue is waiting for automatic recovery review.

pending

A legacy review transaction is processing.

denied

A deterministic guard or comparative review rejected the request.

ready

The approved finalized request is ready for 1Shot.

executing

The platform holds the on-chain execution lease.

bridging

A cross-chain payment was burned and is awaiting Circle's mint on the destination.

failed

The lease is released and the request can retry.

executed

The confirmed EVM transaction hash is recorded.

not_applicable

Execution does not apply, normally because the request was denied.

If the client times out before receiving the POST response, call GET /api/v1/requests?idempotency_key=... or retry the original POST with the same body and idempotency key. Never create a replacement key for the same payment.

An identical replay returns idempotent_replay: true, the same request ID, and the same Base transaction hash without another payment.

A cross-chain payment passes through bridging, and only reaches executed once Circle has minted on the destination chain. The burn alone is not the payee's receipt, so treat the destination hash in bridge.destination_tx_hash as the confirmation. The same request_id can never be both paid locally and bridged.

Amounts and decimals

Send amount as a positive decimal string. Never send JavaScript floating-point values. The server converts amounts using the configured asset decimals, which may be 6 for USDC. Every balance, cap, and history response returns both a display decimal value and the raw on-chain integer units. Requests whose request ID has already been recorded are returned idempotently instead of creating a duplicate spend.

Error codes

All errors return JSON with machine-readable structure and machine-parseable fields when available.

{
  "error": "invalid_api_key",
  "message": "API key is missing or invalid",
  "fields": { "authorization": ["missing bearer token"] },
  "request_id": "abc-123"
}

{
  "error": "agent_mismatch",
  "message": "The agent_address in this request does not match the API key claim",
  "fields": { "agent_address": ["0x... does not match 0x..."] }
}

{
  "error": "insufficient_balance",
  "message": "Delegated balance is below the requested amount",
  "fields": { "amount": ["requested 25.00 USDC, available 4.20 USDC"] }
}

{
  "error": "genlayer_unavailable",
  "message": "submit_request submission failed on GenLayer: ...",
  "fields": {},
  "retryable": true
}

Retry only 502 and 503 responses, honor Retry-After when present, use exponential backoff, and keep the same idempotency key. Do not automatically retry authentication, validation, policy, or conflict errors. Malformed idempotency keys return 422; 409 is reserved for reusing an existing key with different payment data.

Trust model

The agent never receives custody and does not need a wallet library or gas. The owner grants bounded token spending to the platform signer. GenLayer verifies the registered agent and policy. Only approved requests are executed through 1Shot, then the EVM transaction hash is written back to GenLayer. Relayer failures leave on-chain records so requests can be retried safely with no duplicate payout risk.

Two settlement chains are live, both on testnet: Base Sepolia through an ERC-7715 permission relayed by 1Shot, and Arc Testnet through an agent treasury vault the owner deploys and owns. Either can pay out to any CCTP-supported chain in the same Circle environment. Base Mainnet and Arc Mainnet remain disabled until their isolated production configuration, separate signer keys, and live capability checks pass.

A deployment may additionally restrict which policy addresses it serves. When it does, a policy outside that list is refused with 403 policy_not_allowed before any other validation runs, so caps, chains and whitelists are not the cause. That is an operator setting, not something a client can correct — escalate with the request_id.