Pay a person or an agent for finished work, on NEAR.
The money is held under a clause, and the clause is the only key that moves it. Each job gets its own NEAR account with one key, the worker's, which can do exactly one thing: deliver. Delivery pays the worker in the same transaction. If the deadline passes first, the money goes back to whoever paid.
Open the app · Watch the demo · Judges: five-minute path · Threat model · Contract ABI
The demo: one job from invite to payout on the live site, an agent hiring an agent over MCP, 15 refused attacks, and the first clause on mainnet.
Built solo for NEAR HACKATHON, November 2026.
| What it is | Escrow with no escrow agent: a per-job NEAR account whose only key can deliver, and whose code can only pay the worker or refund the buyer |
| Live on mainnet | Factory clausehold.near. Clause r-0.clausehold.near paid 2 real USDC to angelraph.near, then closed itself |
| Live on testnet | clausehold.vercel.app with free test tokens, factory clausehold.testnet owned by a Sputnik DAO |
| For workers | No wallet, no gas. A passkey in the browser is the only key the clause accepts; the desk relays the signed delivery |
| For agents | An MCP server with 7 tools. One agent hires another, funds the job, and pays only for delivered work |
| Earned standing | Every paid job is a line in the worker's public record. New workers are capped at 10; one paid job lifts that to 25, enforced by the contract |
| Proof | npm run judge: 25 checks on testnet, 15 of them attempts to cheat, each refused with its reason code |
| Tracks | agents · realworld · social · security |
- The idea in one minute
- Live on mainnet
- Product tour
- How it works
- Try it in five minutes
- What it looks like on chain
- Why NEAR
- The mandate
- Run it yourself
- For agents
- What can go wrong, and what happens
- Honest limits
A studio, a DAO or an AI agent wants to pay someone for a piece of work: a 30 second cut, a translation, a bug fix. Today it either pays first and hopes, or the worker delivers first and hopes. An escrow service fixes that by holding the money, which means trusting the escrow service, and paying its cut. Agents hiring agents have nobody to trust at all.
| who holds the money | what can go wrong | |
|---|---|---|
| Pay first | the worker | the work never comes |
| Work first | the buyer | the money never comes |
| Escrow service | a company | it freezes, fails, or takes a cut |
| Clausehold | a NEAR account made for this one job | nothing outside its code: pay on delivery, refund at the deadline |
That account has no owner key. Its code allows exactly three exits: to the payee on delivery, back to the buyer on expiry, or back to the buyer after a dispute is settled. Nobody, including us, can take it anywhere else.
The factory is deployed at clausehold.near with a real mandate: native USDC only, at most 5 USDC per clause, new payees at most 3, and only clausehold.near may write clauses. One real job ran end to end with real USDC:
| step | transaction |
|---|---|
| deploy the factory | 57Ujmi4P… |
upload the clause code (same code as testnet, sha256 a765271079ca…) |
5ZXubHJ1… |
| set the mandate | hU8Tq2Ej… |
write r-0.clausehold.near for 2 USDC to angelraph.near |
JDosYTk7… |
| fund it with 2 USDC | H7frpW7B… |
the worker key signs the delivery, clausehold.near relays it; released, angelraph.near +2 USDC |
FCJwGQWG… |
| close: the account deletes itself, 1.986 NEAR back to the buyer | 8bKWE8is… |
Funding from another chain goes through NEAR Intents. A dry 1Click quote for USDC on Base delivering exactly 2 USDC on NEAR asked for 2.0206 USDC and estimated 35 seconds, with refunds set to land on NEAR. npx tsx scripts/mainnet-proof.ts --payee <account> --dry prints the costs of a run without sending anything.
Write a clause. The buyer names the payee, the amount, the deadline and an optional acceptance check. The desk checks it against the treasury's rules before anyone signs.
Accept as the worker. The invite page makes a passkey that becomes the only key the clause will accept. The worker can run the acceptance check before starting.
Funded, then paid. The clause page follows the job on chain, from written to funded to released.
Earned standing. Every account's record of paid jobs, read from the factory, with the limit it has earned.
sequenceDiagram
participant B as Buyer (person, DAO or agent)
participant D as Desk
participant F as Factory clausehold.near
participant C as Clause r-n.clausehold.near
participant W as Worker
B->>D: propose a job (payee, amount, deadline, check)
D-->>B: PASS against the mandate, invite link
W->>D: accept: browser makes the job key (public half only)
B->>F: create_clause (checked again on chain)
F->>C: create account, deploy code, add the one worker key
B->>C: ft_transfer_call (exact amount)
W->>D: signed submit_delivery (meta-transaction)
D->>C: relay it, desk pays the gas
C->>W: pay the worker in the same transaction
C->>F: report released (worker's standing +1)
Note over C: past the deadline instead: anyone calls expire, money back to the buyer
B->>C: close: account deletes itself, storage NEAR back to the buyer
flowchart LR
O[Open] -->|ft_transfer_call| Fu[Funded]
Fu -->|worker key delivers| R[Released: worker paid]
Fu -->|deadline passes, anyone calls expire| X[Returned: buyer refunded]
Fu -->|buyer freezes in review window| Z[Frozen: buyer and DAO co-signer decide]
R --> Cl[Closed: account deleted, deposit back]
X --> Cl
Everything below is on testnet, so it is free.
1. Get a wallet and test tokens. Open https://clausehold.vercel.app/faucet.
- Connect any NEAR wallet (HOT, Meteor, MyNearWallet, Intear and others are listed).
- Get test NEAR from the faucet link on that page.
- Click Get 100 mUSD. You should see your mUSD balance go to 100. mUSD is our test dollar token.
2. Write a clause. Go to Write a clause.
- Payee: any testnet account you control, or
clausehold.testnet. - Amount:
2. Expires after:30. Review window:0. - Click Check and open an invite. You should see Cleared, every mandate rule listed with a tick, and an invite link.
3. Accept as the payee. Open the invite link in a new tab of the same browser and click Accept with a passkey. Your browser makes a key. It is the only key the clause will ever accept, and it can do one thing: submit a delivery. The page shows your new key and says it is waiting for the buyer to write the clause.
4. Write it on chain. Back on the clause page, click Write the clause and approve it in your wallet. You should see a new account r-<n>.clausehold.testnet. Open it on the explorer: it has exactly one access key, a function-call key limited to submit_delivery.
5. Fund it. Click Fund 2.00 mUSD and approve. The clause now holds 2 mUSD and shows Funded.
6. Deliver. On the invite tab, attach any file or type a note and click Submit delivery. There is no wallet prompt and no gas to pay: the passkey signs, and the desk relays it. You should see Paid, and the payee's mUSD goes up by 2.
7. Check the record. Open the Desk. The clause shows Released. Its receipt page and the payee's standing page read straight from the contract.
To see a refusal, go back to step 2 and ask for 12. The clause is refused before anyone signs, and the reason names the rule: a payee who has never been paid may be offered at most 10. Ask for 40 and the per-clause cap of 25 refuses it too. The same rules run inside the factory contract, so calling it directly fails the same way.
A real run from the website with a browser wallet, clause r-2.clausehold.testnet:
| step | transaction |
|---|---|
buyer's wallet calls create_clause; the factory creates r-2, deploys its code and adds the one worker key |
93YxwwVK… |
buyer's wallet sends 2 mUSD with ft_transfer_call; the clause keeps it and turns Funded |
14EjWX8W… |
the payee's key signs submit_delivery as a meta-transaction, the desk relays it, the clause pays the payee and reports to the factory, all in one transaction |
HxM4eEim… |
Clause r-3 went the same way and was then closed: the account deleted itself and its 1.99 NEAR of storage went back to the buyer in 2smpAzwS…. Its receipt now comes from the factory's record.
And one that ran out of time, clause r-0: after its deadline anyone may call expire, and the 2 mUSD went back to the buyer in Eu2Kpjyg….
Each of these does real work in the design. Take one away and the product stops working.
- Sub-accounts. Every job gets its own account,
r-<n>.clausehold.testnet. Its balance is the job's money and nothing else, so it is easy to audit and impossible to mix up with other jobs. - Function-call access keys. The only key on a clause account is the payee's key, limited to calling
submit_deliveryon that one account with no deposit. It cannot transfer, cannot call anything else, and there is no full-access key at all. - Meta-transactions (NEP-366). The payee never needs a wallet or NEAR for gas. Their key signs a delegate action and the desk relays it. The contract checks only that the call came from the clause account itself, which is true only when the worker's key signed it.
- Fungible tokens (NEP-141). Funding is
ft_transfer_call;ft_on_transferkeeps the tokens while the clause is open and refunds anything extra. Payouts register the receiver's storage first, then transfer, and only change state in the final callback, so a failed payout can simply be retried. - Events (NEP-297). Every step logs a
clauseholdevent. The desk mirrors them into a hash-chained log, but the chain is the record. - NEAR Intents (mainnet). A buyer can fund a clause from another chain. The 1Click quote is exact-output, so the clause receives exactly its amount on NEAR, and any refund lands on NEAR too.
- Sputnik DAO. The factory belongs to a DAO. A treasury votes once on the rule, not on every invoice, and the same DAO settles disputes as co-signer.
- NEAR AI Cloud. Turns a plain-English spending rule into a mandate and writes a short note about each delivery. It never decides whether money moves.
A treasury sets its rules once, and every clause is checked against them twice: by the desk before anyone signs, so the refusal can name the rule, and by the factory contract on chain, which is the one that counts. The testnet mandate says:
Stablecoins only. No clause above $25. Anything above $15 needs a second person. At most $50 to any one payee a day and $200 in total a day. A payee who has never delivered gets at most $10. A clause may stay open for at most a week.
The factory reserves each clause's amount against the daily caps when it is written. A returned clause gives the reservation back; a released one keeps it counted.
The buyer can attach a test that every delivery must pass before the desk relays it:
- a public https page must contain some text (the site is live, the post is up)
- a GitHub pull request must be merged (the fix landed)
- the delivery must be an exact file (its SHA-256 is agreed in advance)
The payee sees the check on the invite page and can run it before delivering. A delivery that fails is refused before anything is sent, with the reason (The acceptance check failed: ...), so through the desk it is never paid. The contract itself still only checks who delivered, so a buyer who wants a hard stop also sets a review window and can freeze the clause for the DAO co-signer to decide. Agents attach the same checks through the MCP clausehold_open_job tool.
A payee nobody has paid yet can be offered at most the new-payee ceiling (10 on testnet). Once one clause is released to them, the factory lifts that to the full per-clause cap (25). The factory reads this from its own record of released clauses when a clause is written, so it cannot be skipped, and the desk reads the same record before any wallet prompt. Ask for 12 for a brand-new account and the desk refuses it, naming newPayeeCeiling. Ask for 12 for clausehold.testnet, which has been paid through many released clauses, and it passes.
Every account's record is public: the Standing page ranks payees by released clauses, and each account page shows the limit it has earned and a badge it can embed anywhere, drawn from the chain:
[](https://clausehold.vercel.app/s/clausehold.testnet)On testnet the factory belongs to a Sputnik DAO, clausehold-council.sputnikv2.testnet. The rule changes only when the DAO votes for a proposal that calls set_mandate. Disputes on new clauses are settled with the DAO as co-signer.
| step | transaction |
|---|---|
| create the DAO: one council member, a 1-of-1 vote | Faow7mFx… |
the factory's owner hands it to the DAO with set_owner |
GmxPAgH6… |
proposal 0: make the DAO the co-signer, by calling the factory's set_mandate |
5zRuj9Nb… |
| the council approves it, and the DAO calls the factory | FwFoGeE3… |
The Mandate page reads the owner and the rule straight from the factory. npm run dao:testnet repeats these steps for another DAO; each step checks the chain first and skips what is already done.
You need Node 22 or later.
npm installnpm run dev -- initnpm run dev -- clause editor.testnet 2 --buyer studio-agent.testnet --terms "Cut intro.mp4 to 30 seconds"You should see Verdict: PASS, ten rules ticked, and CLEARED. Change 2 to 12 and you get VETO from newPayeeCeiling. Change it to 40 and maxPerClause refuses it as well.
npm testTo run the desk and the website locally, start the API, then the site in a second terminal, and open http://localhost:5173:
npm run dev -- servecd web && npm install && npm run devThe end-to-end script runs one full clause against the hosted testnet desk: invite, key, writing, funding, a gasless delivery and the release, then checks the payee's balance. It needs a funded testnet account whose key is in ~/.near-credentials/testnet (set CLAUSEHOLD_E2E_BUYER). Each run stores one clause account on chain, which costs about 2 test NEAR.
npm run e2e:testnetThe contracts build and test separately; see contracts/README.md and the full interface in contracts/ABI.md.
Clausehold ships an MCP server, so an AI agent can hire another agent or a person and pay only for finished work. The same server runs on both sides of a job, each with its own NEAR account:
claude mcp add clausehold -e CLAUSEHOLD_AGENT_ACCOUNT=you.testnet -- npx tsx src/mcp/server.tsRun that from the repository after npm install. The account's key is read from ~/.near-credentials/testnet/you.testnet.json (or set CLAUSEHOLD_AGENT_PRIVATE_KEY).
| tool | side | what it does |
|---|---|---|
clausehold_check_payee |
buyer | the payee's record from the factory, and the largest job it has earned |
clausehold_open_job |
buyer | checks the job against the mandate before anything is signed; returns an invite |
clausehold_accept_job |
worker | makes the only key the clause will accept, and binds it; the secret stays on that machine |
clausehold_write_and_fund |
buyer | the buyer's own key writes the clause and sends exactly the job amount |
clausehold_deliver |
worker | signs the delivery as a meta-transaction; the desk pays the gas, and the clause pays the worker at once |
clausehold_status |
either | where the job stands, with an explorer link |
clausehold_close |
either | deletes the settled clause; its storage NEAR goes back to the buyer |
See it work without an AI client: npm run mcp:demo starts two copies of the server, a buyer agent and a worker agent, and runs one job end to end on testnet. A real run, clause r-4.clausehold.testnet:
| step | transaction |
|---|---|
| the buyer agent writes the clause | HJJtDqaj… |
| the buyer agent funds it with 2 mUSD | 4LaVt8Gk… |
| the worker agent delivers with no gas and is paid | BSGXQQyc… |
| the buyer agent closes it and gets the deposit back | vR9FWZBU… |
The worker's standing went from 3 to 4 released clauses in the same run.
An agent can also propose work over HTTP. The desk answers PASS, VETO or ESCALATE before anything is signed.
| route | what it does |
|---|---|
POST /invites |
propose a clause; if it clears, get an invite link for the payee |
GET /invites/:id |
the invite and its status |
POST /invites/:id/key |
the payee's browser posts its public key |
GET /invites/:id/create |
the exact create_clause call for the buyer's wallet to sign |
POST /invites/:id/written |
report the new clause; the desk checks it on chain before linking it |
POST /relay |
submit the payee's signed delivery; the desk pays the gas |
GET /desk/clauses |
every clause and where it stands |
GET /mandate |
the active mandate |
GET /standing/:account |
an account's record from the factory, and the largest clause it has earned |
GET /standing/:account/badge.svg |
an embeddable badge drawn from that record |
GET /leaderboard |
payees ranked by released clauses, from every factory receipt |
The hosted testnet desk is https://clausehold-api-testnet.vercel.app. The routes above are open, because the website calls them without a key. /relay only ever submits a delivery that the payee's own key signed. Routes that decide something (approve, halt) or make the desk sign any other call need an API key. More examples are on the For agents page.
- The payee loses their key. Nothing can be delivered, so after the deadline anyone may call
expireand the money goes back to the buyer. - The desk goes down or turns hostile. It holds no money and no key to any clause. The worst it can do is refuse to relay a delivery, and then the deadline returns the money. A payee can also send their signed delivery through any other relayer.
- Someone calls
submit_deliverydirectly. It fails. Only a call from the clause account itself passes, and only the worker's function-call key can make one. - The work is wrong. With a review window above zero, the buyer may
freezethe clause after delivery. Then it moves only when the buyer and the mandate's co-signer agree. - Tokens arrive late or twice. After a clause settles,
sync_fundingsends any leftover balance to the buyer. - A clause asks for too much. The factory panics with the rule's name, and nothing is created.
The full threat model, with the test behind each control and the known weaknesses, is in docs/THREAT_MODEL.md.
- Writing a clause stores a small contract on chain, which needs about 1.97 NEAR from the buyer while the clause runs. It is a deposit, not a fee: once the clause settles, anyone can close it, and the account deletes itself and sends that NEAR back to the buyer. The buyer still needs the 2 NEAR up front.
- Funding through NEAR Intents is mainnet only, because 1Click has no testnet. On testnet, clauses are funded directly with mUSD. On mainnet the one clause so far was funded directly with USDC; the Intents path was checked with a dry quote, not a paid cross-chain transfer.
- The mainnet factory is owned by
clausehold.nearitself for now, not yet by a DAO. - The testnet DAO has a single council member, so one vote passes a proposal. That proves the wiring; a real treasury would add its members and raise the threshold.
| path | what it is |
|---|---|
contracts/ |
Rust: the factory, the per-job clause contract, a test token, and sandbox tests |
src/ |
the desk: mandate checks, invites, the relayer, the NEAR and Intents clients, the audit log, the CLI |
web/ |
the website: desk, clause pages, the payee's invite page, wallet connect, faucet |
scripts/e2e-testnet.ts |
one full clause against the hosted testnet desk |
tests/ |
unit and API tests for the desk |
This repository previously held CHARTER, a policy layer for AI trading agents on Binance, built in September 2026 for a different hackathon. Everything up to the tag pre-hackathon is that baseline; everything after it is Clausehold, built from November 1 for NEAR HACKATHON. The audit log, the rule-engine shape, the approval flow and the agent keys were kept and rewritten around clauses. All Binance and trading code was removed. The full diff: https://github.com/angelraph/clausehold/compare/pre-hackathon...master
Code in this repository was written with help from Claude, an AI assistant, and reviewed and run by the author.



