Four calls, request to receipt.
Preview Paths follow TAP v0.2. Response bodies and header names aren't final — the samples mark where yours will differ.
- 01
Create
POST /tap/tasksSend a task spec. Mandate checks run before any quote.
- 02
Pay
402 → x402 → retryUnpaid, the API answers 402 Payment Required. Pay in USDC on Base and retry.
- 03
Poll
GET /tap/tasks/:idFUNDED → MATCHING → ACTIVE → FULFILLED.
- 04
Prove
GET /popw/claims/:id/proofFetch the claim, its inclusion proof and the Base transaction. Then check it yourself.
// Preview API — paths follow TAP v0.2; response shapes may change.
import type { PhysicalTaskSpecV02 } from "./teleport-tap-physical-task-types";
import spec from "./shelf-photo.task.json"; // the spec below
const API = process.env.TELEPORT_API_URL!; // issued with preview access, ends in /api/v1
const post = (headers: Record<string, string> = {}) =>
fetch(`${API}/tap/tasks`, {
method: "POST",
headers: { "content-type": "application/json", ...headers },
body: JSON.stringify(spec satisfies PhysicalTaskSpecV02),
});
// 1. Create the task.
let res = await post();
// 2. 402 is the one error that means you're nearly done.
if (res.status === 402) {
const requirements = await res.json(); // asset: USDC, network: Base, amount
const payment = await x402.pay(requirements); // your x402 client
res = await post(payment.headers); // same request, now paid
}
const task = await res.json(); // status: "FUNDED"
// 3. Poll until it's done.
let status = task.status;
while (!["FULFILLED", "CANCELLED", "EXPIRED"].includes(status)) {
await new Promise((r) => setTimeout(r, 15_000));
({ status } = await (await fetch(`${API}/tap/tasks/${task.id}`)).json());
}
// 4. Fetch the proof — then verify it without trusting us (see below).
const proof = await (await fetch(`${API}/popw/claims/${task.claimId}/proof`)).json();Or hand your agent the whole protocol.
The Teleport MCP server gives any MCP client — Claude, ChatGPT, Cursor, your own agent — tools to draft and validate task specs and to verify proofs. Those work today, with no account. Posting tasks switches on with API access.
# Any client that supports remote MCP servers
# (Claude.ai and Claude Desktop connectors, ChatGPT, Cursor, MCP Inspector):
https://mcp.teleport.eco/mcp
# No sign-in needed for the protocol and proof tools.Work today, no account
draft_task_specA schema-valid spec from a plain descriptionvalidate_task_specSchema errors, plus rules the schema can't expressget_referenceExact TAP values and rules, by topiclist_endpointsThe preview API, groupedhash_documentRFC 8785 + domain-separated Keccak-256compute_claim_hashRecompute a claim from its EIP-712 partsverify_merkle_inclusionProve a claim is in an anchored batchget_test_vectorsThe door-assist vectors
With API access (preview)
create_taskPost a spec; returns 402 requirements to your x402 clientget_taskTask statusget_proofClaim, inclusion proof, batch, Base transaction
The proof tools are pure computation, tested against the published vectors — an agent can check a Teleport proof without trusting Teleport. Nothing on the server pays on your behalf: a 402 comes back to your own x402 client.
The task spec.
Sixteen required fields. Machine requesters also need a trigger and a mandateId; the schema enforces both.
Required
- schema
- version
- taskSpecId
- title
- objective
- category
- requestedQuantity
- requester
- fulfillment
- actorRequirement
- completionConditions
- evidenceProfiles
- verificationPolicy
- rightsPolicy
- economics
- popw
Types to import
PhysicalTaskSpecV02RequesterRefPostingMandateFulfillmentSpecCompletionConditionEvidenceProfileEvidenceOriginTaskEconomicsPhysicalTaskStatusPopwClaimV21AssuranceTierMAX_ASSURANCE_BY_ORIGIN
{
"schema": "teleport.tap.physical-task",
"version": "0.2.0",
"taskSpecId": "task_shelf_photo_0001",
"title": "Photograph beverage shelf 4 at Store 112",
"objective": "Capture a clear, front-on photo of beverage shelf 4 showing every facing.",
"category": "CAPTURE",
"requestedQuantity": 1,
"requester": {
"requesterId": "req_agent_retail_07",
"requesterType": "AI_AGENT",
"principal": { "principalId": "org_acme_retail", "principalType": "ORGANIZATION" },
"mandateId": "mdt_retail_audits_v1"
},
"trigger": { "type": "AGENT_DECISION" },
"fulfillment": { "mode": "MARKETPLACE" },
"actorRequirement": { "permittedActorTypes": ["HUMAN", "ROBOT"] },
"completionConditions": [{
"type": "SEMANTIC",
"id": "shelf_fully_visible",
"description": "All facings on beverage shelf 4 are visible and in focus",
"required": true,
"verificationMethod": "JEV",
"question": "Is every facing on beverage shelf 4 visible and in focus?"
}],
"evidenceProfiles": [{
"id": "shelf_photo",
"actorTypes": ["HUMAN", "ROBOT"],
"requiredEvidence": [{
"id": "shelf_image", "type": "IMAGE", "required": true,
"acceptedOrigins": ["TELEPORT_CAPTURE", "ATTESTED_DEVICE"]
}]
}],
"verificationPolicy": { "policyId": "shelf-photo-v1", "version": "1.0" },
"rightsPolicy": { "policyId": "retail-operational-evidence-v1" },
"economics": {
"performerPayUsdCents": 400,
"settlementAssets": ["USDC", "FIAT"],
"requesterPayment": { "method": "X402_USDC", "maxSpendUsdCents": 700 },
"x402": { "required": true, "asset": "USDC", "network": "BASE" }
},
"popw": { "required": true, "assurancePolicyId": "STANDARD" }
}Endpoints.
All under /api/v1. The public proof page lives at /verify/:claimId.
| POST | /tap/quotes | Price a task before posting |
| POST | /tap/tasks | Create a task — may answer 402 |
| GET | /tap/tasks/:id | Task status |
| GET | /tap/tasks/:id/approval | Approval status, when a mandate requires one |
| POST | /tap/mandates | Set limits for a machine requester |
| GET | /tap/mandates/:id | Read a mandate |
| POST | /tap/mandates/:id/suspend | Pause it |
| POST | /tap/mandates/:id/revoke | End it |
| POST | /tap/verifications | Verify work done elsewhere |
| POST | /tap/verifications/:id/evidence | Submit evidence, origin and attestation |
| POST | /tap/triggers | Event in, task out — under mandate |
| GET | /tap/workflows/:id/proof | One proof for a linked workflow |
| GET | /popw/claims/:id/proof | Claim, inclusion proof, batch, Base transaction |
| POST | /popw/verify | Ask Teleport to verify a proof (or do it yourself) |
| GET | /popw/batches/:id | Batch status |
| GET | /proof-network/encoding | Encoding version and type hashes |
Don't take our word for it. Take Keccak-256's.
Three checks, public data only. Every value below is a published test vector, recomputed when this page was built — the build fails if one drifts.
- 01
Recompute the claim hash
MatchHash the 25 claim fields as an EIP-712 struct, then combine with the domain (name “Teleport PoPW”, version “2”, chainId 84532, the registry address).
- domainSeparator
- 0xdb290188e00fc75654d44aeafcec74e206bca02e9c155ff81a7228837fb02346
- structHash
- 0x87858743ecc6a2caceac83b23034be56e7112f607b49f0c996edf486f46ccad6
- keccak256(0x1901 ‖ domain ‖ struct)
- 0xe7e7efca66348e948ca021fd492aa987d23b30dad3ecb4edc01996506553d547
- published claimHash
- 0xe7e7efca66348e948ca021fd492aa987d23b30dad3ecb4edc01996506553d547
- 02
Check Merkle inclusion
MatchThe leaf is the claim hash hashed again. Fold in each sibling, smaller value first. You should land on the batch root.
- leaf = keccak256(claimHash)
- 0x1668f747011c228bda32adadc74e39d7f06c0840bda9548920dccd5b6a9a7904
- sibling 1
- 0xadf994887203373fd65a231fc81e57f1536c7fbdcab55ba599dcdecbdc033d35
- sibling 2
- 0xebedb4f721cc03cd4f5f80ac4d43045bc343d89d80c2edfc146dda12dd56448e
- sibling 3
- 0xe3879f4e8dca5cf0dafebe67da3da78ee02a4fcac12e1afcf4eca993143e66f8
- merkleRoot
- 0x15c33e99ae174567ba28402563b2d6b3f9c641a4280160b25f3a38b5497601c4
- 03
Read the anchor on Base
FinalFind the
BatchAnchoredevent for this batch on the PhysicalWorkRegistry. Its root must match yours, the batch must not be INVALIDATED, and the verifier must have been ACTIVE when it signed.- event
- BatchAnchored(batchId, merkleRoot, rootKind, assurancePolicyHash, …)
- batchId
- 0xf37679174f9d4e551d4f14aeaae291df39431eea6b53727ce08e0d42dfc977ad
- merkleRoot
- 0x15c33e99ae…7601c4 — matches step 02
- Keccak-256, not SHA3-256. They pad differently, so the same input gives a different hash.
- Don't use @openzeppelin/merkle-tree. It builds a different tree. Verify with
MerkleProof.verifyor the reference implementation. - Salts stay private. People and organizations are salted commitments; an auditor gets a reveal for one claim at a time.
For robots, sensors and agents.
Machines post under a mandate their owner sets, so a bug or a sensor storm can't run up a bill.
Authenticate per request
Use
SIGNED_REQUESTorDEVICE_ATTESTATION, not long-lived shared API keys.No wallet keys in the field
Robots and devices pay from a
MANDATE_BUDGETor prepaid balance. Agents can pay with x402. Nobody needs $PORT.Triggers, not loops
POST /tap/triggersturns an event into a task under the mandate's rate limits and spend caps.
Read the protocol on How it works and the proof format on Proof.
Preview access
Get preview access.
We're onboarding agent and robotics teams in small batches. Tell us what you're building.