Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message
api-x-development
api-x-development
darvis/api-x
claude-code
twitter
laravel
oauth1
tweet
twitter-api
x
social-media
mentions
mcp
replies
claude
x-api
model-context-protocol
laravel-mcp
ai-agent
laravel-boost
post-to-x

Work with darvis/api-x. Use it to post a text with an image to X from Laravel code, the x:post command or the post-update MCP tool, to read mentions and answer them with a reply, to write a release announcement, to handle a refused post and to test all of it without calling X.

Install
php artisan boost:add-skill darvis/api-x

Save this content to: .claude/skills/api-x-development/SKILL.md

# darvis/api-x development

## When to use this skill

Use this skill when code or an agent posts to X in an application that has `darvis/api-x` installed, when it reads the posts that mention the account or answers them, when a post is refused with an `XException`, when you announce a release on X, or when you write tests around any of this.

## Setting it up

Run `php artisan x:install`. It asks for the four keys, checks them with `XClient::account()` (one billed read), sets `X_SUBSCRIPTION`, `X_DAILY_LIMIT`, `X_ALLOW_LINKS` and `X_DRY_RUN`, adds the server to `.mcp.json` and ends with a dry run. In a script: `php artisan x:install --consumer-key=... --consumer-secret=... --access-token=... --access-token-secret=... --verify --mcp --no-interaction`.

## How a post runs

`XClient::post($text, $image = null, $dryRun = false)` runs these steps in order and stops at the first failure:

1. Trim the text; an empty text throws `The post text is empty.`
2. Count it the way X does (most Latin characters 1, emoji and CJK 2, a link 23). Above the limit it throws `The post text counts N characters on X; the maximum is M.` The limit is 280, or 25,000 when `X_SUBSCRIPTION` is `basic`, `premium` or `premium_plus`; read it with `XConfig::subscription()->maxLength()`.
3. Without `X_ALLOW_LINKS=true`, a URL, `www.` or a bare domain such as `arvid.nl` throws the link message.
4. When the daily limit is reached it throws `The daily limit of N posts is reached.`
5. Read and check the image: it must exist, be a JPEG, PNG, GIF or WebP and be at most 5 MB.
6. In a dry run, return a `PostResult` with `dryRun` true and no id.
7. Without all four keys it throws `X credentials are missing.`
8. Upload the image to `/2/media/upload` (multipart, `media_category` `tweet_image`), then create the post at `/2/tweets` with the media id.

## Writing a good post

- Keep release announcements short, well under 280 characters, even when the account has an X subscription: one or two sentences and at most three short points. Only go longer when the owner asks for a long post.
- Leave links out. The image and the package name already say what it is about.
- Write in the voice of the account owner, in the language the owner uses on X.

## Reading mentions and replying

`XClient::mentions($sinceId = null, $limit = 10)` (or `x:mentions`, or the MCP tool `list-mentions`) returns the newest posts that mention the account, newest first, without the account's own posts. `$limit` is kept between 5 and 100. A reply to one of the account's posts has `inReplyToId`, and `inReplyToText` when that post is in the `x_posts` history.

| Step | What happens | Cost |
| --- | --- | --- |
| First call | `GET /2/users/me` for the account id, then cached per access token | one billed read, once |
| Every call | `GET /2/users/{id}/mentions` with `since_id` and `max_results` | every post returned is a billed read |
| Reply | `POST /2/tweets` with `reply.in_reply_to_tweet_id` | like any post; with a link much more |

Pitfalls:

- Always pass the newest id of the previous read as `since_id`. Reading without it bills the same posts again on another day.
- Never poll mentions on a schedule of minutes or on page loads; read when someone asks.
- X only allows a reply to a post that mentions or quotes the account. Reply to a mention, not to some other post in the thread, or X answers 403.
- A reply counts towards `X_DAILY_LIMIT` and is refused with a link unless `X_ALLOW_LINKS=true`.
- `X_DRY_RUN` does not stop reading; it only concerns posting.

## Writing a good reply

- Show every reply to the owner before sending it, unless the owner said to send without asking. It is public, under their name.
- Answer the person, not the crowd: short, specific, in the owner's voice and language. One or two sentences is usually right.
- Not everything needs an answer: a plain "nice!" can get a short thanks, spam gets nothing. Leave prices, complaints and legal matters to the owner.
- Don't start with the @handle; X adds the reply context itself.

@verbatim
<code-snippet name="Answer the newest mentions" lang="php">
use Darvis\ApiX\XClient;

$client = app(XClient::class);

foreach ($client->mentions(sinceId: $lastSeenId) as $mention) {
    // Draft an answer, let the owner approve it, then:
    $client->post('Yes, it works with Laravel 13.', replyTo: $mention->id);
}
</code-snippet>
@endverbatim

## Announcing a release

After a tag is pushed, build the post from the new section of `CHANGELOG.md` and attach the release card GitHub makes for every release:

@verbatim
<code-snippet name="Release announcement" lang="php">
use Darvis\ApiX\XClient;

app(XClient::class)->post(
    "darvis/api-x 1.1.0 is out.\n\n• Posts with a GIF\n• Clearer error for a read only token",
    'https://opengraph.githubassets.com/1/ArvidDeJong/api-x/releases/tag/v1.1.0',
);
</code-snippet>
@endverbatim

Through MCP the same post is the `post-update` tool with `text` and `image`.

## Handling a refusal

Every failure is an `XException`. When X itself refuses, the message starts with `X refused to create the post` or `X refused to upload the image` and holds the HTTP status and X's reason. A 403 usually means the app has no Write permission or the access token predates it; 402 means no API credit; 429 means wait. Don't retry a 4xx in a loop.

## Testing

@verbatim
<code-snippet name="Fake X in a test" lang="php">
use Illuminate\Support\Facades\Http;

Http::fake([
    'api.x.com/2/media/upload' => Http::response(['data' => ['id' => '10']]),
    'api.x.com/2/tweets' => Http::response(['data' => ['id' => '20']], 201),
    'api.x.com/2/users/me' => Http::response(['data' => ['id' => '1', 'name' => 'You', 'username' => 'you']]),
    'api.x.com/2/users/1/mentions*' => Http::response(['data' => [
        ['id' => '30', 'text' => '@you Nice!', 'author_id' => '2'],
    ]]),
]);
</code-snippet>
@endverbatim

Use the `array` cache store so the daily limit starts at zero, and `Http::assertNothingSent()` after a dry run.

darvis/api-x development

When to use this skill

Use this skill when code or an agent posts to X in an application that has darvis/api-x installed, when it reads the posts that mention the account or answers them, when a post is refused with an XException, when you announce a release on X, or when you write tests around any of this.

Setting it up

Run php artisan x:install. It asks for the four keys, checks them with XClient::account() (one billed read), sets X_SUBSCRIPTION, X_DAILY_LIMIT, X_ALLOW_LINKS and X_DRY_RUN, adds the server to .mcp.json and ends with a dry run. In a script: php artisan x:install --consumer-key=... --consumer-secret=... --access-token=... --access-token-secret=... --verify --mcp --no-interaction.

How a post runs

XClient::post($text, $image = null, $dryRun = false) runs these steps in order and stops at the first failure:

  1. Trim the text; an empty text throws The post text is empty.
  2. Count it the way X does (most Latin characters 1, emoji and CJK 2, a link 23). Above the limit it throws The post text counts N characters on X; the maximum is M. The limit is 280, or 25,000 when X_SUBSCRIPTION is basic, premium or premium_plus; read it with XConfig::subscription()->maxLength().
  3. Without X_ALLOW_LINKS=true, a URL, www. or a bare domain such as arvid.nl throws the link message.
  4. When the daily limit is reached it throws The daily limit of N posts is reached.
  5. Read and check the image: it must exist, be a JPEG, PNG, GIF or WebP and be at most 5 MB.
  6. In a dry run, return a PostResult with dryRun true and no id.
  7. Without all four keys it throws X credentials are missing.
  8. Upload the image to /2/media/upload (multipart, media_category tweet_image), then create the post at /2/tweets with the media id.

Writing a good post

  • Keep release announcements short, well under 280 characters, even when the account has an X subscription: one or two sentences and at most three short points. Only go longer when the owner asks for a long post.
  • Leave links out. The image and the package name already say what it is about.
  • Write in the voice of the account owner, in the language the owner uses on X.

Reading mentions and replying

XClient::mentions($sinceId = null, $limit = 10) (or x:mentions, or the MCP tool list-mentions) returns the newest posts that mention the account, newest first, without the account's own posts. $limit is kept between 5 and 100. A reply to one of the account's posts has inReplyToId, and inReplyToText when that post is in the x_posts history.

Step What happens Cost
First call GET /2/users/me for the account id, then cached per access token one billed read, once
Every call GET /2/users/{id}/mentions with since_id and max_results every post returned is a billed read
Reply POST /2/tweets with reply.in_reply_to_tweet_id like any post; with a link much more

Pitfalls:

  • Always pass the newest id of the previous read as since_id. Reading without it bills the same posts again on another day.
  • Never poll mentions on a schedule of minutes or on page loads; read when someone asks.
  • X only allows a reply to a post that mentions or quotes the account. Reply to a mention, not to some other post in the thread, or X answers 403.
  • A reply counts towards X_DAILY_LIMIT and is refused with a link unless X_ALLOW_LINKS=true.
  • X_DRY_RUN does not stop reading; it only concerns posting.

Writing a good reply

  • Show every reply to the owner before sending it, unless the owner said to send without asking. It is public, under their name.
  • Answer the person, not the crowd: short, specific, in the owner's voice and language. One or two sentences is usually right.
  • Not everything needs an answer: a plain "nice!" can get a short thanks, spam gets nothing. Leave prices, complaints and legal matters to the owner.
  • Don't start with the @handle; X adds the reply context itself.

@verbatim use Darvis\ApiX\XClient;

$client = app(XClient::class);

foreach ($client->mentions(sinceId: $lastSeenId) as $mention) { // Draft an answer, let the owner approve it, then: $client->post('Yes, it works with Laravel 13.', replyTo: $mention->id); } @endverbatim

Announcing a release

After a tag is pushed, build the post from the new section of CHANGELOG.md and attach the release card GitHub makes for every release:

@verbatim use Darvis\ApiX\XClient;

app(XClient::class)->post( "darvis/api-x 1.1.0 is out.\n\n• Posts with a GIF\n• Clearer error for a read only token", 'https://opengraph.githubassets.com/1/ArvidDeJong/api-x/releases/tag/v1.1.0', ); @endverbatim

Through MCP the same post is the post-update tool with text and image.

Handling a refusal

Every failure is an XException. When X itself refuses, the message starts with X refused to create the post or X refused to upload the image and holds the HTTP status and X's reason. A 403 usually means the app has no Write permission or the access token predates it; 402 means no API credit; 429 means wait. Don't retry a 4xx in a loop.

Testing

@verbatim use Illuminate\Support\Facades\Http;

Http::fake([ 'api.x.com/2/media/upload' => Http::response(['data' => ['id' => '10']]), 'api.x.com/2/tweets' => Http::response(['data' => ['id' => '20']], 201), 'api.x.com/2/users/me' => Http::response(['data' => ['id' => '1', 'name' => 'You', 'username' => 'you']]), 'api.x.com/2/users/1/mentions*' => Http::response(['data' => [ ['id' => '30', 'text' => '@you Nice!', 'author_id' => '2'], ]]), ]); @endverbatim

Use the array cache store so the daily limit starts at zero, and Http::assertNothingSent() after a dry run.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager