---
name: wurk-metaplex-pda-skill
version: 1.1.0
description: Client guide for WURK Metaplex PDA-backed x402 endpoints.
homepage: https://wurk.fun
metadata: {"openclaw":{"category":"payments","api_base":"https://wurkapi.fun"}}
---

# WURK Metaplex PDA Skill

WURK is a microjob platform where agents and humans can create paid growth jobs and collect real submissions through API-first flows.

This document explains how to use WURK's Metaplex PDA-backed Solana x402 endpoints as a client or agent.

## Document URLs

- Primary: `https://wurkapi.fun/metaplexpdaskill.md`
- Main WURK doc: `https://wurkapi.fun/skill.md`
- Metaplex welcome doc: `https://wurkapi.fun/metaplexwelcome.md`

## Support Chat via Job Secret

For agent support, WURK also exposes shared secret-auth support endpoints:

- `POST /api/agent-support/send`
- `GET /api/agent-support/messages`

This is wallet-threaded support chat:

- use any paid job `secret` (including secrets from Metaplex PDA-paid jobs)
- WURK resolves secret -> paid job -> wallet thread
- different secrets from the same wallet resolve to the same chat thread

Input/rate-limit rules:

- `message` required, max `2000` chars
- max `1` message per `10` seconds per wallet
- max `20` messages per hour per wallet

Send example:

```bash
curl -X POST "https://wurkapi.fun/api/agent-support/send" \
  -H "Content-Type: application/json" \
  -d '{"secret":"YOUR_JOB_SECRET","message":"Need help with this PDA-paid job."}'
```

## Advanced Agent-to-Human Family (Cross-reference)

If you also need policy-driven agent-help jobs (non-PDA route family), use:

- `GET|POST /{network}/agenttohumanadvanced?action=create|view|recover`
- `GET|POST /mpp/agenttohumanadvanced` and `GET|POST /mpp-solana/agenttohumanadvanced`
- creator winner selection: `POST /api/agenttohumanadvanced/choose-winners` with `secret` + `submissionIds`
- SIWX free recover routes: `GET /{network}/siwx/agenttohuman/recover` (alias: `/{network}/siwx/agenthelp/recover`)
- SIWX recover uses `SIGN-IN-WITH-X` auth and returns the same recover payload shape as paid `/agenttohuman/recover`

See the main WURK skill for full advanced policy guidance:

- `https://wurkapi.fun/skill.md`

## What These Endpoints Are

These routes let a client pay from a Metaplex agent signer PDA while still using an x402-style flow:

1. request a route
2. receive `402 Payment Required`
3. build and sign the payment transaction
4. retry the exact returned `resource.url` with `PAYMENT-SIGNATURE`

This is not the normal off-chain Solana x402 exact flow where a wallet directly signs a standard transfer by itself. Here:

- the payment is an MPL Core `execute` instruction
- that `execute` wraps one SPL Token `TransferChecked`
- the USDC source account belongs to the Metaplex signer PDA
- the off-chain signer is the wallet that has execute authority over the Metaplex asset
- WURK co-signs as fee payer and submits the transaction

## When To Use This

This workflow is only needed when you want to pay from a Metaplex signer PDA on WURK.
If you want normal x402 payments from your authority wallet, use the main guide: `https://wurkapi.fun/skill.md`.

Terminology note:

- In this document, `pda` means the **Metaplex agent signer PDA** (the payment PDA used as transfer authority).
- This is the PDA linked to your agent asset for payment flows.
- Do not confuse it with agent identity/registry references; those are not the payment PDA input for these routes.

Use these routes when:

- your agent has a Metaplex asset
- that asset has a signer PDA that holds USDC
- you control a wallet that can execute for that asset

Do not use these routes when:

- you want the standard Solana x402 flow
- you can already pay directly from a normal wallet
- you do not have Metaplex execute authority

## Available Routes

### Agent-to-Human (PDA, Basic)

`GET|POST /metaplex/pda/solana/agenttohuman`

- purpose: hire real humans for normal feedback/opinion/review tasks with standard agent-to-human defaults
- this creates a normal WURK **custom job** under the hood (you get `secret` + `statusUrl` to read submissions)
- `action=create|view|recover` (default: `create`)
- requires `pda` for paid `create` and `recover` flows
- basic defaults and constraints:
  - default `winners=10`
  - default/minimum `perUser=0.025` USDC
  - `winners` range: `1..100`
  - selection window is fixed to 60 minutes
  - max entries are set automatically to about `1.2 x winners`
- write `description` in clear plain language so workers can quickly understand the exact task
- if a task needs files (images/video/docs), include a public hosted link directly in `description`

Quick usage:

- Create (descriptor): call without `description` to receive `402` schema + payment requirements
- Create (bound): provide `description` + optional `winners`/`perUser` + `pda`, then pay with `PAYMENT-SIGNATURE`
- View (free): `action=view&secret=...`
- Recover (paid utility): `action=recover&pda=...`

### Agent-to-Human Advanced (PDA)

`GET|POST /metaplex/pda/solana/agenttohumanadvanced`

- purpose: hire real humans for feedback/opinions/review tasks with advanced policy controls
- this creates a normal WURK **custom job** under the hood (you still get `secret` + `statusUrl` for submissions)
- `action=create|view|recover` (default: `create`)
- canonical PDA advanced route for policy-aware agent-help jobs
- requires `pda` for paid `create` and `recover` flows
- write the job `description` in clear plain language so any normal internet user can understand exactly what to do
- creator-mode winner selection stays shared via:
  - `POST /api/agenttohumanadvanced/choose-winners` using `secret` + `submissionIds`
- if you want workers to review images/videos/files, put a **public hosted link** in `description` (for example CDN/S3/Drive direct links)
- only share links workers can open without private login; otherwise they cannot complete the task correctly

Quick usage:

- Create (descriptor): call without `description` to receive `402` schema + payment requirements
- Create (bound): provide `description` + advanced fields + `pda`, then pay with `PAYMENT-SIGNATURE`
- View (free): `action=view&secret=...`
- Recover (paid utility): `action=recover&pda=...`

### X / Twitter

`GET|POST /metaplex/pda/solana/xlikes`

- Inputs: `url`, `amount`, `pda`
- Amount is provided on the root route

`GET|POST /metaplex/pda/solana/xcomments`

- Inputs: `url`, `amount`, `pda`
- Also supports:
  - `/metaplex/pda/solana/xcomments-:winners`
  - `/metaplex/pda/solana/xcomments/:winners`

`GET|POST /metaplex/pda/solana/xreposts`

- Inputs: `url`, `amount`, `pda`
- Also supports:
  - `/metaplex/pda/solana/xreposts-:winners`
  - `/metaplex/pda/solana/xreposts/:winners`

`GET|POST /metaplex/pda/solana/xbookmarks`

- Inputs: `url`, `amount`, `pda`
- Also supports:
  - `/metaplex/pda/solana/xbookmarks-:winners`
  - `/metaplex/pda/solana/xbookmarks/:winners`

`GET|POST /metaplex/pda/solana/xfollowers`

- Inputs: `handle`, `amount`, `pda`
- `handle` may be an X handle, profile URL, or X community URL

`GET|POST /metaplex/pda/solana/xraid`

- Preset routes:
  - `/metaplex/pda/solana/xraid`
  - `/metaplex/pda/solana/xraid/small`
  - `/metaplex/pda/solana/xraid/medium`
  - `/metaplex/pda/solana/xraid/large`
- Custom routes:
  - `/metaplex/pda/solana/xraid/custom`
  - `/metaplex/pda/solana/xraid/custom/:likes/:reposts/:comments/:bookmarks`
- Scout routes:
  - `/metaplex/pda/solana/xraid/scout/small`
  - `/metaplex/pda/solana/xraid/scout/medium`
  - `/metaplex/pda/solana/xraid/scout/large`
  - `/metaplex/pda/solana/xraid/scout/custom`
  - `/metaplex/pda/solana/xraid/scout/custom/:likes/:reposts/:comments/:bookmarks`
- Inputs depend on variant, but always include `url` and `pda`

### Instagram

`GET|POST /metaplex/pda/solana/insta-likes`

- Inputs: `url`, `amount`, `pda`
- Also supports:
  - `/metaplex/pda/solana/insta-likes-:winners`
  - `/metaplex/pda/solana/insta-likes/:winners`

`GET|POST /metaplex/pda/solana/instacomments`

- Inputs: `url`, `amount`, `pda`
- Optional: `instructions`
- Also supports:
  - `/metaplex/pda/solana/instacomments-:winners`
  - `/metaplex/pda/solana/instacomments/:winners`

`GET|POST /metaplex/pda/solana/instafollowers`

- Inputs: `handle`, `amount`, `pda`
- `handle` may be an Instagram handle or profile URL
- Also supports:
  - `/metaplex/pda/solana/instafollowers-:winners`
  - `/metaplex/pda/solana/instafollowers/:winners`

### YouTube

`GET|POST /metaplex/pda/solana/ytlikes`

- Inputs: `url`, `amount`, `pda`
- Also supports:
  - `/metaplex/pda/solana/ytlikes-:winners`
  - `/metaplex/pda/solana/ytlikes/:winners`

`GET|POST /metaplex/pda/solana/ytcomments`

- Inputs: `url`, `amount`, `pda`
- Optional: `instructions`
- Also supports:
  - `/metaplex/pda/solana/ytcomments-:winners`
  - `/metaplex/pda/solana/ytcomments/:winners`

`GET|POST /metaplex/pda/solana/ytsubs`

- Inputs: `handle`, `amount`, `pda`
- `handle` may be a YouTube handle or channel URL
- Also supports:
  - `/metaplex/pda/solana/ytsubs-:winners`
  - `/metaplex/pda/solana/ytsubs/:winners`

### Dexscreener

`GET|POST /metaplex/pda/solana/dex-rocket`

- Inputs: `url`, `amount`, `pda`
- Also supports:
  - `/metaplex/pda/solana/dex-rocket-:winners`
  - `/metaplex/pda/solana/dex-rocket/:winners`

## Shared Client Flow

All PDA routes follow the same client flow.

### 1. Discovery mode

Call the route without its main target field.

Examples:

```bash
curl -i "https://wurkapi.fun/metaplex/pda/solana/xlikes"
curl -i "https://wurkapi.fun/metaplex/pda/solana/instafollowers"
curl -i "https://wurkapi.fun/metaplex/pda/solana/ytcomments"
```

You receive:

- HTTP `402`
- a `Payment-Required` header
- a JSON body with:
  - `accepts`
  - `resource`
  - `extensions.bazaar`

Use that schema to learn what inputs the route expects.

### 2. Create-bind mode

Call the route with the required target fields and `pda`, but without `PAYMENT-SIGNATURE`.

Example:

```bash
curl -i "https://wurkapi.fun/metaplex/pda/solana/ytcomments?url=https://www.youtube.com/watch?v=abc123&amount=5&instructions=mention+the+editing&pda=PDA_ADDRESS"
```

You receive another HTTP `402`, but this one is now bound to:

- the concrete `jobId`
- the resolved Metaplex asset
- the exact request parameters for that job

This is the payment contract for that specific job.

Use the JSON response body as your canonical payment contract object.
Do not prefer a decoded `Payment-Required` header object over the JSON body when both are available.

### 3. Payment mode

Retry the exact returned `resource.url` with:

- `PAYMENT-SIGNATURE: <encoded x402 payment payload>`

On success you receive HTTP `200` with fields such as:

- `jobId`
- route-specific quantity field like `likes`, `comments`, `followers`, `subscribers`, or `rockets`
- `transaction`
- `payer`
- `asset`
- `pda`

### 4. Replay mode

If the same paid request is retried after a successful settlement, the route returns the same successful result instead of charging again.

## Critical Rules

### Retry the exact returned `resource.url`

Do not rebuild the payment URL from memory.

The returned `resource.url` contains bound data such as:

- `jobId`
- resolved Metaplex `asset`
- original target fields
- `amount`
- `pda`

### Use the exact returned `accepts[0]`

Do not mutate the chosen `accepts[0]`.

The PDA verifier compares the signed payment payload against the route's expected requirement.

### Distinguish the two assets

There are two different assets in the response:

- `accepted.asset`
  - this is the payment asset, usually USDC
- `accepted.extra.asset`
  - this is the Metaplex asset used in `mpl-core execute`

Do not confuse them.

### Use the returned RPC when available

If `accepted.extra.rpcUrl` is present, use that RPC URL while building the transaction.

This reduces stale blockhash mismatches.

### Build, sign, and send immediately

Do not prebuild the transaction and wait.

The safest flow is:

1. fetch the bound `402`
2. build the transaction immediately
3. sign immediately
4. send the payment retry immediately

### The PDA does not sign off-chain

The off-chain signer must be a wallet that has execute authority over the Metaplex asset, such as:

- the asset owner
- the asset authority
- a delegated executive wallet

The PDA itself is only the on-chain transfer authority inside the wrapped instruction.

## Exact Transaction Shape

The client should build a Solana transaction with exactly:

1. `ComputeBudgetProgram.setComputeUnitLimit`
2. `ComputeBudgetProgram.setComputeUnitPrice`
3. one MPL Core `execute` instruction

The server accepts any integer `ComputeUnitLimit` in the range `1..100000`.

That `execute` must wrap exactly one SPL Token `TransferChecked`:

```text
ComputeBudgetProgram.setComputeUnitLimit(...)
ComputeBudgetProgram.setComputeUnitPrice(...)
MPL Core execute(
  asset = accepted.extra.asset,
  instructions = [
    SPL Token TransferChecked(
      source = ATA(accepted.asset, pda),
      mint = accepted.asset,
      destination = ATA(accepted.asset, accepted.payTo),
      authority = pda,
      amount = accepted.amount,
      decimals = 6
    )
  ]
)
```

For collection-bound assets, the `execute` instruction may also include the asset collection account.
The server validates both strict forms:

- execute without collection (non-collection assets)
- execute with the correct collection (collection-bound assets)

Collection extraction guidance:

- check both `updateAuthority.type` and `updateAuthority.__kind` for `"Collection"`
- read collection address from `updateAuthority.address`, with fallback to `updateAuthority.fields[0]`

Performance note:

- Normal successful payments stay on the fast path.
- Collection metadata checks are only expanded when execute matching fails or when simulation indicates a collection-related issue.

Important:

- fee payer must equal `accepted.extra.feePayer`
- the signer must sign the versioned transaction off-chain
- `setComputeUnitLimit` must be within `1..100000`
- `setComputeUnitPrice` must be exactly `1` microlamport
- if the asset is collection-bound, include the correct collection in `execute`
- do not add extra instructions
- do not reorder instructions
- do not use address lookup tables

## Generic Client Algorithm

```text
Step 1: request the route without its main target field
  -> receive discovery 402

Step 2: request the route with target fields + amount + pda
  -> receive bound 402
  -> save:
     - resource.url
     - accepts[0]
     - accepts[0].extra.asset
     - accepts[0].extra.feePayer
     - accepts[0].extra.rpcUrl
     - accepts[0].amount
     - accepts[0].payTo

Step 3: build an execute-wrapped SPL TransferChecked
  -> source authority is the PDA
  -> fee payer is accepted.extra.feePayer
  -> sign with the wallet that can execute for the Metaplex asset

Step 4: retry the exact returned resource.url
  -> send PAYMENT-SIGNATURE

Step 5: if the server says stale blockhash
  -> fetch a fresh blockhash
  -> rebuild the same transaction
  -> sign again
  -> retry the same bound resource.url

Step 6: receive 200 success
```

## Recommended Packages

For a TypeScript or JavaScript client, this small package set is usually enough:

- `@x402/core`
  - for x402 header encoding such as `PAYMENT-SIGNATURE`
- `@solana/web3.js`
  - for versioned transactions, blockhashes, and Solana instruction building
- `@solana/spl-token`
  - for `TransferChecked` and associated token account helpers
- `@metaplex-foundation/mpl-core`
  - for the Metaplex Core `execute` instruction
- `@metaplex-foundation/umi`
  - for Metaplex instruction wiring
- `@metaplex-foundation/umi-bundle-defaults`
  - for creating a UMI client quickly
- `bs58`
  - useful for decoding Solana private keys from base58 strings

You do not have to use this exact package set. Other Solana stacks are fine too, as long as your client produces the same transaction shape and reuses the exact returned `resource.url` and `accepted` object.

## Generic TypeScript Skeleton

```typescript
import { encodePaymentSignatureHeader } from "@x402/core/http";
import {
  ComputeBudgetProgram,
  Connection,
  Keypair,
  PublicKey,
  TransactionInstruction,
  TransactionMessage,
  VersionedTransaction,
} from "@solana/web3.js";
import {
  TOKEN_PROGRAM_ID,
  createTransferCheckedInstruction,
  getAssociatedTokenAddressSync,
} from "@solana/spl-token";
import { createUmi } from "@metaplex-foundation/umi-bundle-defaults";
import { execute, mplCore } from "@metaplex-foundation/mpl-core";
import { createSignerFromKeypair, publicKey, signerIdentity } from "@metaplex-foundation/umi";

const REQUIRED_DECIMALS = 6;
const PDA_COMPUTE_UNIT_LIMIT_MIN = 1;
const PDA_COMPUTE_UNIT_LIMIT_MAX = 100_000;
const DEFAULT_COMPUTE_UNIT_LIMIT = 30_000;
const DEFAULT_COMPUTE_UNIT_PRICE_MICROLAMPORTS = 1;

function fromWeb3JsInstruction(instruction: TransactionInstruction) {
  return {
    keys: instruction.keys.map((accountMeta) => ({
      pubkey: accountMeta.pubkey.toBase58(),
      isSigner: accountMeta.isSigner,
      isWritable: accountMeta.isWritable,
    })),
    programId: instruction.programId.toBase58(),
    data: new Uint8Array(instruction.data),
  };
}

async function payMetaplexPdaRoute(input: {
  baseUrl: string;
  path: string;
  params: Record<string, string>;
  authority: Keypair;
}) {
  const createUrl = new URL(input.path, input.baseUrl.replace(/\/$/, ""));
  for (const [key, value] of Object.entries(input.params)) {
    createUrl.searchParams.set(key, value);
  }

  const createRes = await fetch(createUrl, {
    headers: { Accept: "application/json" },
  });
  if (createRes.status !== 402) {
    throw new Error(`Expected 402, got ${createRes.status}`);
  }

  let paymentRequired = await createRes.json();
  const accepted = paymentRequired.accepts[0];
  const computeUnitLimit = Math.min(
    PDA_COMPUTE_UNIT_LIMIT_MAX,
    Math.max(PDA_COMPUTE_UNIT_LIMIT_MIN, DEFAULT_COMPUTE_UNIT_LIMIT),
  ); // any integer in 1..100000 is valid

  const usdcMint = new PublicKey(String(accepted.asset));
  const metaplexAsset = String(accepted.extra.asset);
  const feePayer = new PublicKey(String(accepted.extra.feePayer));
  const pda = new PublicKey(String(accepted.extra.pda));
  const payTo = new PublicKey(String(accepted.payTo));
  const rpcUrl = String(accepted.extra.rpcUrl || "");
  if (!rpcUrl) throw new Error("missing rpc url");

  const sourceAta = getAssociatedTokenAddressSync(usdcMint, pda, true, TOKEN_PROGRAM_ID);
  const destinationAta = getAssociatedTokenAddressSync(usdcMint, payTo, true, TOKEN_PROGRAM_ID);

  const transferIx = createTransferCheckedInstruction(
    sourceAta,
    usdcMint,
    destinationAta,
    pda,
    BigInt(accepted.amount),
    REQUIRED_DECIMALS,
    [],
    TOKEN_PROGRAM_ID,
  );

  const umi = createUmi(rpcUrl).use(mplCore());
  const umiKeypair = umi.eddsa.createKeypairFromSecretKey(input.authority.secretKey);
  const signer = createSignerFromKeypair(umi, umiKeypair);
  umi.use(signerIdentity(signer));

  const builder = execute(umi, {
    asset: { publicKey: publicKey(metaplexAsset) },
    instructions: [fromWeb3JsInstruction(transferIx)],
  });

  const executeIx = builder.getInstructions()[0] as any;
  const web3ExecuteIx = new TransactionInstruction({
    keys: executeIx.keys.map((key: any) => ({
      pubkey: new PublicKey(key.pubkey),
      isSigner: key.isSigner,
      isWritable: key.isWritable,
    })),
    programId: new PublicKey(executeIx.programId),
    data: Buffer.from(executeIx.data),
  });

  const connection = new Connection(rpcUrl, "finalized");
  const { blockhash } = await connection.getLatestBlockhash("finalized");

  const message = new TransactionMessage({
    payerKey: feePayer,
    recentBlockhash: blockhash,
    instructions: [
      ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnitLimit }),
      ComputeBudgetProgram.setComputeUnitPrice({ microLamports: DEFAULT_COMPUTE_UNIT_PRICE_MICROLAMPORTS }),
      web3ExecuteIx,
    ],
  }).compileToV0Message();

  const tx = new VersionedTransaction(message);
  tx.sign([input.authority]);

  const paymentPayload = {
    x402Version: paymentRequired.x402Version,
    resource: paymentRequired.resource,
    accepted,
    payload: {
      transaction: Buffer.from(tx.serialize()).toString("base64"),
    },
    ...(paymentRequired.extensions ? { extensions: paymentRequired.extensions } : {}),
  };

  const paidRes = await fetch(String(paymentRequired.resource.url), {
    headers: {
      Accept: "application/json",
      "PAYMENT-SIGNATURE": encodePaymentSignatureHeader(paymentPayload),
    },
  });

  const paidText = await paidRes.text();
  if (!paidRes.ok) throw new Error(paidText);
  return JSON.parse(paidText);
}
```

## Common Errors

- `url required`
- `handle required`
- `amount required`
- `pda required`
- `amount must be between ...`
- `no Metaplex agent asset found for the supplied pda`
- `multiple Metaplex agent assets match this pda; ambiguous asset resolution`
- `missing bound payment metadata; call once without PAYMENT-SIGNATURE first and retry the returned resource.url`
- `bound asset does not match the supplied pda`
- `job not found`
- `job does not match the supplied url`
- `job does not match the supplied handle`
- `job does not match the supplied amount`
- `job does not match the supplied instructions`
- `invalid PAYMENT-SIGNATURE header`
- `errorCode: stale_blockhash`
- `retryable: true`
- `errorCode: pda_collection_required`
- `errorDetail: Missing required collection account in execute. Required collection: <COLLECTION_PUBKEY>`

For stale blockhash responses:

- fetch a fresh blockhash
- rebuild the transaction
- sign again
- retry the same bound `resource.url`

For `pda_collection_required` responses:

- rebuild the transaction with the required collection account included in the MPL Core `execute` instruction
- sign again
- retry the same bound `resource.url`

## Practical Guidance

- Always treat the create-step `402` as the real payment contract.
- Always read live values from the most recent `402`.
- Never hardcode `jobId`, `asset`, `amount`, `payTo`, `feePayer`, or `rpcUrl`.
- Keep the authority wallet secret safe.
- Keep enough USDC in the PDA token account.
- If you do not need PDA payments, use the standard x402 flow from `https://wurkapi.fun/skill.md`.

## Links

- API root: `https://wurkapi.fun`
- Metaplex PDA skill doc: `https://wurkapi.fun/metaplexpdaskill.md`
- Main WURK doc: `https://wurkapi.fun/skill.md`
- Metaplex welcome doc: `https://wurkapi.fun/metaplexwelcome.md`
- WURK website: `https://wurk.fun`
