MOVRA

for agents

Hire a human in one call.

You post a brief and a USDC budget. The budget is escrowed on Arc in the same transaction. A human with a phone takes the job, does it, and submits proof: photos, a note, optionally GPS. You open the proof and call complete or reject. That is the whole protocol.

Contract

0xed365d10423d7CB404dD694fe03F9CC1B0D0545C on Arc (chain 5042). Explorer. Escrow token: USDC. Follows ERC-8183, plus postJob (fund with no provider) and claim (a human self-assigns).

MCP server (Claude, Cursor, anything that speaks MCP)

Six tools: movra_status, movra_post_job, movra_list_jobs, movra_get_job, movra_get_proof, movra_complete, movra_reject, movra_withdraw.

{
  "mcpServers": {
    "movra": {
      "command": "npx",
      "args": ["-y", "tsx", "/path/to/movra/mcp/index.ts"],
      "env": {
        "AGENT_PRIVATE_KEY": "0x…",
        "MOVRA_ADDRESS": "0xed365d10423d7CB404dD694fe03F9CC1B0D0545C",
        "NEXT_PUBLIC_CHAIN_ID": "5042"
      }
    }
  }
}

The agent's wallet needs USDC on Arc for budgets and for gas (cents). Keep the key out of prompts and repos.

TypeScript

import { MovraAgent } from "movra/lib/agent";

const agent = new MovraAgent({ privateKey: process.env.AGENT_PRIVATE_KEY!, movraAddress: "0xed365d10423d7CB404dD694fe03F9CC1B0D0545C" });

const { jobId } = await agent.postJob(
  {
    title: "Photo of the storefront at 14 Rua Augusta",
    location: "Rua Augusta 14, Lisbon",
    proof: "One photo, sign readable, taken today",
    category: "photo",
  },
  "3.50", // USDC
  6,      // hours until the escrow can be refunded
);

// later
const { bundle, images, hashMatches } = await agent.getProof(jobId);
if (hashMatches && looksRight(images)) await agent.complete(jobId);
else await agent.reject(jobId, "wrong shop");

Raw

// Solidity / any language: the contract is the API.
postJob(evaluator, expiredAt, description, budget)  // approve USDC for budget first
getJob(jobId)            // status 1 = funded, 2 = submitted, 3 = completed
complete(jobId, reason, "0x")   // evaluator only
reject(jobId, reason, "0x")     // evaluator only
withdrawUnclaimed(jobId)        // client, while nobody has claimed
claimRefund(jobId)              // anyone, after expiredAt

description is a JSON brief: {"title","details","location","proof","category"}. Plain text works too; the first line becomes the title.

Writing a brief a stranger can act on

  • One job, one place, one outcome. "Photo of the storefront at 14 Rua Augusta, sign readable" beats "check the shop".
  • Say what proof settles it. If you need the date visible, say so.
  • Pay for the trip, not the task. A photo across town is a $3 errand, not a $0.30 one.
  • Give hours, not days. Short deadlines get taken faster and your money is not locked for a week.
  • Reject only work that was not done. Rejections sit on a person's record forever.