mars-plugged
v0.2.0
Published
Raw Solana + x402 toolkit: talk to Anchor programs with no IDL, decode accounts from a byte-offset table, and build x402 payment envelopes by hand.
Maintainers
Readme
plugged
Hackathon toolkit for the estate rooms. Two tracks: Solana (rooms 01, 04) — query the chain, decode raw account bytes, derive PDAs, call Anchor programs; and x402 (rooms 02, 03, 05) — read 402 terms in full, build signed payment envelopes by hand, follow paid trails.
Setup
cd ~/mars-dev/plugged
bun install
bun test # 62 tests, all offline.env:
OPERATIVE_ID=OPERATIVE-07 # required for x402; every clause value derives from it
SOLANA_RPC=https://api.devnet.solana.com # optional; falls back to the Solana CLI's config, then devnet
SOLANA_KEYPAIR=~/.config/solana/id.json # optional; the event box sets this at login
BRIEF_URL=http://10.61.0.20:8700 # optional; the live-parameters endpoint
OPENAI_API_KEY=*** # for the agent
MODEL=gpt-4oThe event runs on Solana devnet. RPC resolves as SOLANA_RPC → the Solana CLI's own
json_rpc_url → devnet, so a preconfigured box is used automatically. The websocket endpoint is
derived as RPC port + 1; don't pin it.
CLI
bun run scripts/cli.ts <command> [args...]Read
| Command | What |
|---|---|
| balance [ADDRESS] | SOL balance |
| account <ADDRESS> | Raw account bytes, owner, lamports |
| tokens [OWNER] | SPL token accounts and balances |
| ata <WALLET> <MINT> | Derive Associated Token Account address |
| decode <ADDRESS> '<JSON>' | Decode account using offset field table |
| pda <PROGRAM> <SEED...> | Derive PDA address |
| pda-fetch <PROGRAM> <SEED...> | Derive PDA and fetch its account data |
Seed encodings for pda/pda-fetch — a pubkey seed is its 32 raw bytes, never its base58 text:
| Form | Bytes |
|---|---|
| claim | UTF-8 |
| pk:<base58> | 32 raw bytes |
| hex:<hex> | decoded hex |
| u64:<n> | 8 little-endian bytes |
Field types for decode: pubkey, u8, u16, u32, u64, i64, bool, anchor_string, and — with a length — bytes (hex) and utf8 (text, for an 8-char code held as [u8; 8]).
Hash / Encode
| Command | What |
|---|---|
| hash sha256 <HEX> | SHA256 of raw bytes |
| hash xor <HEX_A> <HEX_B> | Byte-wise XOR (equal lengths) |
| hash xor-key <HEX> <KEY> | Repeating-key XOR (short key) |
| hash ctr <HEX> <KEY> | SHA-256 counter-mode XOR (symmetric) |
| hash crc32 <HEX> | CRC-32, to verify a decryption worked |
| hash disc <INSTRUCTION> | Anchor instruction discriminator |
| borsh '<JSON>' | Borsh-encode args to hex |
| borsh-disc <NAME> | Same as hash disc |
Write
| Command | What |
|---|---|
| transfer <SRC_ATA> <DST_ATA> <AMT> [MEMO] | Send SPL tokens, with optional memo in same tx |
| anchor <PROG> <IX> '<ARGS>' '<ACCTS>' [MEMO] | Build and send a raw Anchor instruction |
| confirm <SIGNATURE> | Check transaction status |
x402 / Network
| Command | What |
|---|---|
| brief <room> | Live parameters for your operative ID |
| operative | Your ID in both capitalisations |
| fetch <URL> | Status + every header + raw body |
| x402 get <URL> | Same, with ?operative= appended |
| x402 pay <URL> <B64> | Retry with the envelope in the X-PAYMENT header |
| envelope transfer <SRC> <DST> <AMT> [MEMO] | Sign without sending → base64 |
| envelope anchor <PROG> <IX> '<ARGS>' '<ACCTS>' [MEMO] | Same, for a program call |
fetch deliberately prints status, headers and raw body rather than parsed JSON: a decryption key can arrive in a response header, and a green 200 can carry a failure in its body.
LLM Agent
Natural language interface to read and write:
bun run scripts/agent.ts "check my balance and token accounts"
bun run scripts/agent.ts "decode account ABC with fields: authority at offset 8 as pubkey, amount at offset 40 as u64"
bun run scripts/agent.ts "transfer 1000000 base units from my SCRIP ATA to tribute ATA with memo 'the exact memo string'"Script Modules
Import directly for room scripts that need precise control:
| Module | Exports |
|---|---|
| scripts/decode.ts | decodeAccount(data, fields) — offset table decoder, bounds-checked by field name |
| scripts/pda.ts | findPDA(programId, seeds), parseSeed(s), pkSeed(pk), printPDA() |
| scripts/tx.ts | buildAnchorTx(), signAndSerialize(), sendTransferWithMemo(), sendTransfer() |
| scripts/anchor.ts | buildAnchorInstruction(), discriminator(), borshEncode() |
| scripts/hash.ts | sha256(), sha256FirstN(), nonceBytes(), xor(), fromHex()/toHex(), fromBase64()/toBase64() |
| scripts/x402.ts | request(), get(), pay(), withOperative(), idCases(), operativeId() |
| scripts/config.ts | ESTATE_RPC, resolveRpcUrl(), getConnection(), loadKeypair() |
| scripts/bootstrap.ts | me (Keypair), conn (Connection) |
Room Pattern
A typical room script:
import { me, conn } from "./scripts/bootstrap";
import { decodeAccount } from "./scripts/decode";
import { sendTransferWithMemo } from "./scripts/tx";
// 1. Fetch clause
const clause = await (await fetch("http://10.61.0.20:3000/clause/4")).json();
// 2. Fetch and decode the target account
const info = await conn.getAccountInfo(new PublicKey(clause.account));
const decoded = decodeAccount(info!.data, clause.fields);
// 3. Derive your ATA
import { getAssociatedTokenAddress } from "@solana/spl-token";
const myAta = await getAssociatedTokenAddress(new PublicKey(clause.mint), me.publicKey);
// 4. Build and send (transfer + memo in ONE transaction)
await sendTransferWithMemo(conn, me, {
sourceAta: myAta,
destAta: new PublicKey(decoded.tribute_ata),
amount: decoded.amount,
memo: clause.memo,
});Key Addresses
| Program | Address |
|---|---|
| SPL Token | TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA |
| Associated Token Account | ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL |
| SPL Memo | MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr |
| Instructions Sysvar | Sysvar1nstructions1111111111111111111111111 |
Pitfalls
- Endianness. Multi-byte ints are little-endian.
readBigUInt64LE, never BE. - Raw bytes, not hex strings.
sha256(bytes), notsha256(hexString). - Base units, not decimals. Send the integer the clause states; never multiply by 10^decimals.
- Memo in same transaction. The SDK transfer helper omits it. Build the tx yourself with both instructions.
- ATA is already a token account. A
tribute_atafrom a decoded record is the destination — don't derive an ATA on top of it. - PDA seeds. Exact order, case, and encoding. Empty
getAccountInfousually means wrong seeds. - Decode fields in order. Wrong width on an early field shifts everything after it.
- Read the body, not the status code. A green
200and a payment that really left your wallet can still come back SEALED, with the fee kept. - Headers carry values. A key to unscramble a piece can be in a response header, not the body.
?operative=on every request, or endpoints return nothing useful.- ID capitalisation varies per endpoint. Some want it exactly as issued, some lowercase. That is not a typo in the brief — use
operativeto see both. - Never hardcode. Every value is live and belongs to you; re-read it rather than pasting it forward.
