remit.md Elixir SDK

Skill MD · Docs · Agent Spec

CI

Universal payment protocol for AI agents - Elixir client.

Installation

# mix.exs
{:remitmd, "~> 0.1"}

Quickstart

# Testing - no keys, no network
{:ok, mock} = RemitMd.MockRemit.start_link()
RemitMd.MockRemit.set_balance(mock, "0xYourAgent", "500.00")

wallet = RemitMd.Wallet.new(mock: mock, address: "0xYourAgent")
{:ok, tx} = RemitMd.Wallet.pay(wallet, "0xRecipient", "1.50")
IO.puts("Sent! status: #{tx.status}")
# Production - set REMITMD_KEY environment variable
wallet = RemitMd.Wallet.from_env()
{:ok, tx} = RemitMd.Wallet.pay(wallet, "0xRecipient", "1.50")
IO.puts("tx_hash: #{tx.tx_hash}")

CLI Signer (Recommended)

The CLI signer delegates key management to the remit CLI, which holds your encrypted keystore at ~/.remit/keys/. No private key in the environment.

export REMIT_SIGNER_KEY=your_password
# Explicit
{:ok, signer} = RemitMd.CliSigner.new()
wallet = RemitMd.Wallet.new(signer: signer)

# Or auto-detect from env (recommended)
wallet = RemitMd.Wallet.from_env() # detects remit CLI automatically

Wallet.from_env() detects signer credentials automatically. Priority: remit CLI > REMITMD_KEY.

Permits (Gasless USDC Approval)

All payment methods auto-sign EIP-2612 permits when no explicit permit is provided. The server computes the EIP-712 hash via /permits/prepare; the SDK only signs the hash.

# Auto-permit (recommended) - just call the method, permit is handled internally
{:ok, tx} = RemitMd.Wallet.pay(wallet, "0xRecipient", "5.00")

# Manual permit - sign via server-side /permits/prepare
permit = RemitMd.Wallet.sign_permit(wallet, "direct", 5.0)
{:ok, tx} = RemitMd.Wallet.pay(wallet, "0xRecipient", "5.00", permit: permit)

Auto-permit works on: pay, create_escrow, create_tab, create_stream, create_bounty, place_deposit.

Payment Models

Function Model Use Case
Wallet.pay/4 Direct One-off payments
Wallet.create_escrow/4 Escrow Milestone-gated work
Wallet.open_tab/4 Tab High-frequency micro-payments
Wallet.create_stream/4 Stream Continuous per-second payments
Wallet.post_bounty/3 Bounty Open tasks any agent can claim
Wallet.place_deposit/4 Deposit Refundable collateral

Testing with MockRemit

MockRemit is an OTP GenServer that implements the full payment API in-memory. Tests complete in microseconds - no network, no blockchain.

defmodule MyAgentTest do
  use ExUnit.Case

  test "agent pays for inference" do
    {:ok, mock} = RemitMd.MockRemit.start_link()
    RemitMd.MockRemit.set_balance(mock, "0xAgent", "100.00")

    wallet = RemitMd.Wallet.new(mock: mock, address: "0xAgent")
    {:ok, _tx} = RemitMd.Wallet.pay(wallet, "0xProvider", "0.003")

    assert RemitMd.MockRemit.was_paid?(mock, "0xProvider")
    assert RemitMd.MockRemit.total_paid_to(mock, "0xProvider") == "0.003"
  end
end

Escrow Example

wallet = RemitMd.Wallet.from_env()

# Create escrow
{:ok, esc} = RemitMd.Wallet.create_escrow(wallet, "0xContractor", "200.00",
  milestones: ["design_approved", "implementation_done", "tests_passing"],
  description: "Build data pipeline"
)

# Release milestones as work completes
{:ok, esc} = RemitMd.Wallet.pay_milestone(wallet, esc.escrow_id, "design_approved")
{:ok, esc} = RemitMd.Wallet.pay_milestone(wallet, esc.escrow_id, "implementation_done")
{:ok, esc} = RemitMd.Wallet.pay_milestone(wallet, esc.escrow_id, "tests_passing")
# esc.status == "complete"

Configuration

Env Var Default Description
REMITMD_KEY - secp256k1 private key (required unless using local signer)
REMITMD_CHAIN"base" Chain: base, base_sepolia
REMITMD_API_URL(chain default) Override API base URL

Custom Signer

Implement RemitMd.Signer to use HSM, KMS, or hardware wallets:

defmodule MyKmsSigner do
  @behaviour RemitMd.Signer

  @impl true
  def sign(_signer, message), do: MyKms.sign(message)

  @impl true
  def address(_signer), do: "0xYourAgentAddress"
end

wallet = RemitMd.Wallet.new(signer: MyKmsSigner, chain: "base")

Additional Methods

# Contract discovery (cached per session)
{:ok, contracts} = RemitMd.Wallet.get_contracts(wallet)

# Tab provider (signing charges)
sig = RemitMd.Wallet.sign_tab_charge(wallet, tab_contract, tab_id, total_charged, call_count)
{:ok, charge} = RemitMd.Wallet.charge_tab(wallet, tab_id, amount, cumulative, call_count, provider_sig)
{:ok, tab} = RemitMd.Wallet.close_tab(wallet, tab_id)

# Webhooks
{:ok, wh} = RemitMd.Wallet.register_webhook(wallet, "https://...", ["payment.received"])

# Operator links (optional: messages: [], agent_name: "")
{:ok, link} = RemitMd.Wallet.create_fund_link(wallet)
{:ok, link} = RemitMd.Wallet.create_withdraw_link(wallet, messages: ["Withdraw"], agent_name: "my-agent")

# Testnet funding
{:ok, result} = RemitMd.Wallet.mint(wallet, 100)  # $100 testnet USDC

Error Handling

Errors return {:error, %RemitMd.Error{}} with machine-readable codes and actionable details:

case RemitMd.Wallet.pay(wallet, "0xRecipient", "100.00") do
  {:ok, tx} -> IO.puts("Paid: #{tx.tx_hash}")
  {:error, %{code: "INSUFFICIENT_BALANCE"} = err} ->
    IO.puts("Need more USDC: #{err.message}")
    # Enriched: "Insufficient USDC balance: have $5.00, need $100.00"
  {:error, err} -> IO.puts("Error [#{err.code}]: #{err.message}")
end

Requirements

License

MIT