Non-custodial stablecoin payments. Get paid in USDC and EURC, wherever you already sell.
A payment gateway where every payment gets its own address, the chain proves the payment, and the money goes to your vault or your wallet. Built on Arc.
Live site · MCP server · Docs · API guide · Arc mainnet contracts
- Why Plinth
- How a payment works
- Features
- Live on Arc mainnet
- Three ways to be paid
- Fees
- AI-native: the hosted MCP server
- Networks and tokens
- Architecture
- Repository layout
- Quick start
- Quality: what was tested
- Security and status
- Documentation
Accepting stablecoins usually means one of two compromises: a custodial processor that holds your balance, or a hand-rolled "send to this wallet" page where you match transfers to orders yourself.
Plinth removes both. Each order gets a unique deposit address that exists before any contract does (a counterfactual CREATE2 address), so there is nothing to fund and nothing to babysit. A watcher verifies the transfer on-chain, and a restricted relayer pays the gas to sweep it to its destination. The gateway holds no key that can move merchant funds.
- For merchants: one API call per order, a hosted checkout, signed webhooks, payment links and invoices, a dashboard, and your money lands in an account you control.
- For customers: pay from a connected wallet with one gasless signature (EIP-3009), or just send USDC to the address or QR code.
- For developers and AI assistants: a REST API, a hosted MCP server with 29 tools, and
llms.txt.
sequenceDiagram
autonumber
participant S as Your server
participant P as Plinth API
participant C as Customer
participant A as Invoice address (CREATE2)
participant R as Relayer
participant D as Your vault or wallet
S->>P: POST /v1/payment-intents (amount, order_reference)
P-->>S: checkout_url + deposit addresses
S->>C: redirect to checkout_url
C->>A: pays USDC (wallet signature, or a plain transfer)
P->>P: watcher: log, receipt, official token, finality
R->>A: deployAndSweep() (one transaction, relayer pays gas)
A->>D: net amount (the fee goes to the treasury in the same transaction)
P-->>S: signed webhook: payment.confirmed, then payment.completed
The address is computed offline and cross-checked against the deployed factory's predict() before a customer ever sees it. sweep() takes no arguments, so nobody, including the gateway, can choose a recipient.
| Payments | Unique address per payment per network, partial / repeated / late / over / wrong-token payments handled, reorgs reversed, expiry (20 minutes by default) |
| Checkout | Hosted page, wallet checkout (gasless, EIP-3009) and address / QR checkout, live status from the backend only |
| Merchant tools | Payment links, numbered invoices, refunds verified on-chain, webhooks (signed, retried, idempotent), ledger, settlements, audit log, team roles |
| Tokens | USDC on every network, EURC on Arc, each with its own contract set and vault |
| Payout modes | Vault, direct to your own wallet, or (operator opt-in) custodial |
| Fee | 0.20% by default, capped at 1% by the contract, taken inside the settlement transaction |
| AI-native | Hosted MCP server (29 tools), llms.txt, integration playbook, ready prompts |
| Reliability | Idempotent API, persisted raw transactions, fee replacement, nonce recovery, kill switch, deployment self-check every 5 minutes |
The contracts are deployed on Arc (chain id 5042), byte-verified against the build, and approved for production. Source of truth: deployments/arc-mainnet.json.
| Contract | USDC set | EURC set |
|---|---|---|
| VaultFactory | 0xeBc9…D56D |
0x0A31…B2Ea |
| InvoiceDeposit (implementation) | 0x7950…ea84 |
0x8e33…7a9f |
| InvoiceFactory | 0xb106…a168 |
0xc8a6…ad65 |
| PaymentRouter | 0xf154…0728 |
0x134C…5Dfb |
Tokens: USDC 0x3600…0000 (Arc's native USDC, 6-decimal interface) and EURC 0xbEf5…21c1. Treasury (fee recipient): 0xC0d1…Adc2. The API reads the deployed contracts back from the chain (token, chain id, version, treasury, fee cap) and closes the network if anything differs.
| Mode | Where the money goes | Control |
|---|---|---|
| Vault (default) | A MerchantVault contract only your owner wallet can withdraw from; optional delayed forwarding rules |
Your owner key. Use a multisig or hardware wallet |
| Direct | Straight to a payout wallet you proved you control (EIP-4361 signature, fresh sign-in, cooldown) | Your wallet key |
| Custodial | Into the platform's custody wallet; you withdraw to a proven wallet, manually or on a schedule | Off by default; an operator must enable it |
The destination is part of each invoice address, so changing mode only affects new payments. The vault's delay applies to forwarding rules only; the owner key can always withdraw at once.
The platform fee is 20 bps (0.20%), rounded down in the token's own units, taken in the same transaction that settles the payment and sent to the treasury. The contract fixes a maximum of 100 bps (1%) that cannot be changed after deployment. If the treasury transfer ever fails, the merchant receives the full amount and the event FeeSkipped records it: a hostile treasury can never block a settlement. The fee is not refunded. Details: docs/architecture.md.
Plinth ships an MCP server so an AI assistant can add checkout to your project and run your payments in plain English. It is hosted: one URL, your key in a header, nothing to install.
# Claude Code
claude mcp add --transport http plinth https://plinth-api.zerolayer.online/mcp --header "Authorization: Bearer sk_test_..."{ "mcpServers": { "plinth": { "url": "https://plinth-api.zerolayer.online/mcp", "headers": { "Authorization": "Bearer sk_test_..." } } } }29 tools over payments, payment links, invoices, refunds, webhooks and reports, plus the integration playbook, a ready prompt and resources. There is no tool that changes where money goes, creates keys or edits the team. Full reference: docs/mcp.md.
The registry holds 26 networks: Arc, Base, Arbitrum, OP, Ethereum, Polygon and 18 more Circle-native USDC chains, each with its chain id, token addresses, EIP-712 domain, finality rule and log window verified read-only against the live chain. Only Arc has contracts deployed today; every other network stays closed until it is deployed and approved. Table, verification method and what is pending: docs/networks.md.
flowchart LR
subgraph Customer
W[Wallet]
end
subgraph Chain[Arc and other EVM chains]
F[InvoiceFactory] --> I[Invoice clone<br/>CREATE2, immutable args]
R[PaymentRouter]
V[MerchantVault]
end
subgraph Gateway
API[API and MCP]
WK[Watcher]
RL[Relayer<br/>gas only]
DB[(Postgres ledger)]
end
W -->|USDC| I
W -->|signed authorization| R
WK -->|verify receipts| Chain
RL -->|deployAndSweep| F
I -->|net amount| V
API --- DB
WK --- DB
RL --- DB
- Contracts (Solidity 0.8.28, Foundry, OpenZeppelin 5): ERC-1167 clones with immutable arguments; no proxies, no admin, no upgrade path.
- Engine: payment status is derived from verified facts; the ledger is idempotency-keyed; webhooks are signed and retried.
- Relayer: simulates before signing, quotes live fees, enforces per-network spend limits, persists the signed transaction before broadcasting.
Full design: docs/architecture.md. Threats and trade-offs: docs/threat-model.md.
| Path | What |
|---|---|
contracts/ |
Foundry project: MerchantVault, VaultFactory, InvoiceDeposit, InvoiceFactory, PaymentRouter, tests, deploy script |
packages/shared |
Network registry, fee and token model, state machine, money, schemas |
packages/blockchain |
viem adapter: offline CREATE2 derivation, EIP-3009, log scanning, dev chain |
packages/db |
Drizzle schema and migrations (Postgres; PGlite for local and tests) |
packages/engine |
Payments, ledger, settlement planner, relayer, watcher, refunds, payout wallets, custody, auth |
packages/mcp |
The MCP server (tools, Streamable HTTP handler, stdio) |
apps/api |
Fastify REST API (/v1), the hosted MCP endpoint (/mcp), OpenAPI |
apps/worker |
Background loop: watcher, relayer, forwarding, expiry, webhooks |
apps/web |
Next.js: marketing site, hosted checkout, merchant dashboard |
deployments/ |
Verified per-network deployment records (public addresses only) |
scripts/ |
Dev stack, deploy finalize and approve, network verification, end-to-end run |
docs/ |
Architecture, threat model, API guide, runbooks, review, MCP, networks |
Requirements: Node 22 or newer, pnpm, Foundry.
pnpm install
(cd contracts && forge build) && pnpm abis # compile and export ABIs
pnpm dev:stack # local chain + contracts + embedded Postgres + API :4000 + worker + demo merchant
pnpm web # dashboard and checkout at http://localhost:3000 (separate terminal)dev:stack prints a demo login, a test API key and the local contract addresses. Create a payment:
curl -X POST localhost:4000/v1/payment-intents -H "Authorization: Bearer <test key>" \
-H "Idempotency-Key: demo-1" -H "Content-Type: application/json" -d '{"amount":"25.00","order_reference":"DEMO-1"}'Open the returned checkout_url. Deploying contracts to a network: docs/DEPLOY-HANDOFF.md and docs/deploy-testnet.md.
| Suite | Result |
|---|---|
Foundry (pnpm test:contracts) |
87 passed: unit, fuzz, invariants, fee split and fail-open fee, vault and direct kinds, router nonce binding, two-token deployments, gas-griefing fuzz |
| Heavy campaign | 20,000 fuzz runs per test and 1,000 invariant runs at depth 100: no counterexample |
| Static analysis | Slither, 80 detectors: no high or medium finding |
TypeScript (pnpm test) |
engine 120, api 28, blockchain 23, shared 23, mcp 8, db 6, all passing |
End to end (pnpm e2e:local) |
vault, address payment, wallet payment, direct payout and EURC on a local chain, with merchant and treasury balances checked on-chain |
Network probes (pnpm verify:networks) |
chain id, token identity, EIP-712 domain, EIP-3009 and permit selectors, log window |
What was not done is listed in docs/verification.md.
- Not independently audited. An internal review (tools, findings, fixes) is in docs/security-review.md; it is not an audit. Start with small amounts.
- A compromised backend cannot take funds already at issued addresses or in vaults, but it could hand out new checkout addresses that pay an attacker. The threat model says exactly what is and is not protected.
- Keys: the relayer key is a gas-only environment key; there is no KMS/HSM yet. Custody is off by default.
- Report a vulnerability privately; see the Security page of the site.
docs/README.md indexes everything: architecture · API guide + OpenAPI · MCP · networks · operations · threat model · security review · verification · deploy handoff.
Built for Arc. License: not yet specified.